handle user preferences and name
also use consistent users and just get them from the class instead of passing them around fixes CORE-271 fixes CORE-272 test plan - merge a user with prefs - they should come through Change-Id: I1debabbffed1dba8b1c215aa676a51a5b5dc4c53 Reviewed-on: https://gerrit.instructure.com/187281 Tested-by: Jenkins Reviewed-by: Brent Burgoyne <bburgoyne@instructure.com> QA-Review: Brent Burgoyne <bburgoyne@instructure.com> Product-Review: Rob Orton <rob@instructure.com>
This commit is contained in:
parent
8f1058183a
commit
3ff025385d
|
@ -81,230 +81,255 @@ class SplitUsers
|
|||
context_id: 'courses.id'}.freeze
|
||||
].freeze
|
||||
|
||||
attr_accessor :source_user, :restored_user, :merge_data
|
||||
|
||||
def initialize(source_user, merge_data)
|
||||
@source_user = source_user
|
||||
@merge_data = merge_data
|
||||
@restored_user = nil
|
||||
end
|
||||
|
||||
def self.split_db_users(user, merge_data = nil)
|
||||
if merge_data
|
||||
users = split_users(user, merge_data)
|
||||
users = new(user, merge_data).split_users
|
||||
else
|
||||
users = []
|
||||
UserMergeData.active.splitable.where(user_id: user).shard(user).find_each do |data|
|
||||
splitters = split_users(user, data)
|
||||
splitters = new(user, data).split_users
|
||||
users = splitters | users
|
||||
end
|
||||
end
|
||||
users
|
||||
end
|
||||
|
||||
class << self
|
||||
private
|
||||
|
||||
def split_users(user, merge_data)
|
||||
# user is the active user that was the destination of the user merge
|
||||
user.shard.activate do
|
||||
ActiveRecord::Base.transaction do
|
||||
records = merge_data.user_merge_data_records
|
||||
old_user = User.find(merge_data.from_user_id)
|
||||
old_user, pseudonyms = restore_old_user(old_user, records)
|
||||
records = check_and_update_local_ids(old_user, records) if merge_data.from_user_id > Shard::IDS_PER_SHARD
|
||||
records = records.preload(:context)
|
||||
restore_merge_items(old_user)
|
||||
move_records_to_old_user(user, old_user, records, pseudonyms)
|
||||
# update account associations for each split out user
|
||||
users = [old_user, user]
|
||||
User.update_account_associations(users, all_shards: (old_user.shard != user.shard))
|
||||
merge_data.destroy
|
||||
User.where(id: users).touch_all
|
||||
users
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def restore_merge_items(old_user)
|
||||
Shard.with_each_shard(old_user.associated_shards + old_user.associated_shards(:weak) + old_user.associated_shards(:shadow)) do
|
||||
UserPastLtiIds.where(user: old_user, user_lti_id: old_user.lti_id).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def check_and_update_local_ids(old_user, records)
|
||||
if records.where("previous_user_id<?", Shard::IDS_PER_SHARD).where(previous_user_id: old_user.local_id).exists?
|
||||
records.where(previous_user_id: old_user.local_id).update_all(previous_user_id: old_user.global_id)
|
||||
end
|
||||
records.reload
|
||||
end
|
||||
|
||||
# source_user is the destination user of the user merge
|
||||
# user is the old user that is being restored
|
||||
def move_records_to_old_user(source_user, user, records, pseudonyms)
|
||||
fix_communication_channels(source_user, user, records.where(context_type: 'CommunicationChannel'))
|
||||
move_user_observers(source_user, user, records.where(context_type: ['UserObserver', 'UserObservationLink'], previous_user_id: user))
|
||||
move_attachments(source_user, user, records.where(context_type: 'Attachment'))
|
||||
enrollment_ids = records.where(context_type: 'Enrollment', previous_user_id: user).pluck(:context_id)
|
||||
Shard.partition_by_shard(enrollment_ids) do |enrollments|
|
||||
restore_enrollments(enrollments, source_user, user)
|
||||
end
|
||||
Shard.partition_by_shard(pseudonyms) do |pseudonyms|
|
||||
move_new_enrollments(enrollment_ids, pseudonyms, source_user, user)
|
||||
end
|
||||
handle_submissions(source_user, user, records)
|
||||
account_users_ids = records.where(context_type: 'AccountUser').pluck(:context_id)
|
||||
AccountUser.where(id: account_users_ids).update_all(user_id: user.id)
|
||||
restore_workflow_states_from_records(records)
|
||||
end
|
||||
|
||||
def restore_enrollments(enrollments, source_user, user)
|
||||
enrollments = Enrollment.where(id: enrollments).where.not(user: user)
|
||||
move_enrollments(enrollments, source_user, user)
|
||||
end
|
||||
|
||||
def move_new_enrollments(enrollment_ids, pseudonyms, source_user, user)
|
||||
new_enrollments = Enrollment.where.not(id: enrollment_ids, user: user).
|
||||
where(sis_pseudonym_id: pseudonyms).shard(pseudonyms.first.shard)
|
||||
move_enrollments(new_enrollments, source_user, user)
|
||||
end
|
||||
|
||||
def move_enrollments(enrollments, source_user, user)
|
||||
enrollments_to_update = filter_enrollments(enrollments, user)
|
||||
Enrollment.where(id: enrollments_to_update).update_all(user_id: user.id, updated_at: Time.now.utc)
|
||||
courses = enrollments_to_update.map(&:course_id)
|
||||
transfer_enrollment_data(source_user, user, Course.where(id: courses))
|
||||
move_submissions(source_user, user, enrollments_to_update)
|
||||
end
|
||||
|
||||
def filter_enrollments(enrollments, user)
|
||||
enrollments.reject do |e|
|
||||
# skip conflicting enrollments
|
||||
Enrollment.where(user_id: user,
|
||||
course_section_id: e.course_section_id,
|
||||
type: e.type,
|
||||
role_id: e.role_id).where.not(id: e).shard(e.shard).exists?
|
||||
end
|
||||
end
|
||||
|
||||
# source_user is the destination user of the user merge
|
||||
# user is the old user that is being restored
|
||||
def fix_communication_channels(source_user, user, cc_records)
|
||||
if source_user.shard != user.shard
|
||||
source_user.shard.activate do
|
||||
# remove communication channels that didn't exist prior to the merge
|
||||
ccs = CommunicationChannel.where(id: cc_records.where(previous_workflow_state: 'non_existent').pluck(:context_id))
|
||||
DelayedMessage.where(communication_channel_id: ccs).delete_all
|
||||
NotificationPolicy.where(communication_channel: ccs).delete_all
|
||||
ccs.delete_all
|
||||
end
|
||||
end
|
||||
# move moved communication channels back
|
||||
max_position = user.communication_channels.last.try(:position) || 0
|
||||
scope = source_user.communication_channels.where(id: cc_records.where(previous_user_id: user).pluck(:context_id))
|
||||
scope.update_all(["user_id=?, position=position+?", user.id, max_position]) unless scope.empty?
|
||||
|
||||
cc_records.where.not(previous_workflow_state: 'non existent').each do |cr|
|
||||
CommunicationChannel.where(id: cr.context_id).update_all(workflow_state: cr.previous_workflow_state)
|
||||
end
|
||||
end
|
||||
|
||||
def move_user_observers(source_user, user, records)
|
||||
# skip when the user observer is between the two users. Just undlete the record
|
||||
not_obs = UserObservationLink.where(user_id: [source_user, user], observer_id: [source_user, user])
|
||||
obs = UserObservationLink.where(id: records.pluck(:context_id)).where.not(id: not_obs)
|
||||
|
||||
source_user.as_student_observation_links.where(id: obs).update_all(user_id: user.id)
|
||||
source_user.as_observer_observation_links.where(id: obs).update_all(observer_id: user.id)
|
||||
end
|
||||
|
||||
def move_attachments(source_user, user, records)
|
||||
attachments = source_user.attachments.where(id: records.pluck(:context_id))
|
||||
Attachment.migrate_attachments(source_user, user, attachments)
|
||||
end
|
||||
|
||||
def restore_old_user(user, records)
|
||||
pseudonyms_ids = records.where(context_type: 'Pseudonym').pluck(:context_id)
|
||||
pseudonyms = Pseudonym.where(id: pseudonyms_ids)
|
||||
user ||= User.create!(name: pseudonyms.first.unique_id)
|
||||
user.workflow_state = 'registered'
|
||||
user.save!
|
||||
move_pseudonyms_to_user(pseudonyms, user)
|
||||
return user, pseudonyms
|
||||
end
|
||||
|
||||
def move_pseudonyms_to_user(pseudonyms, target_user)
|
||||
pseudonyms.each do |pseudonym|
|
||||
pseudonym.update_attribute(:user_id, target_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
def transfer_enrollment_data(source_user, target_user, courses)
|
||||
# use a partition proc so that we only run on the actual course shard, not all
|
||||
# shards associated with the course
|
||||
Shard.partition_by_shard(courses, ->(course) { course.shard }) do |shard_course|
|
||||
source_user_id = source_user.id
|
||||
target_user_id = target_user.id
|
||||
ENROLLMENT_DATA_UPDATES.each do |update|
|
||||
relation = update[:table].classify.constantize.all
|
||||
relation = relation.instance_exec(&update[:scope]) if update[:scope]
|
||||
|
||||
relation.
|
||||
where((update[:context_id] || :context_id) => shard_course,
|
||||
(update[:foreign_key] || :user_id) => source_user_id).
|
||||
update_all((update[:foreign_key] || :user_id) => target_user_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# source_user is the destination user of the user merge
|
||||
# user is the old user that is being restored
|
||||
# enrollments are enrollments that have been created since the merge event,
|
||||
# but for a pseudonym that was moved back to the old user.
|
||||
# Also work that has happened since the merge event should moved if the
|
||||
# enrollment is moved.
|
||||
def move_submissions(source_user, user, enrollments)
|
||||
# there should be no conflicts here because this is only called for
|
||||
# enrollments that were updated which already excluded conflicts, but we
|
||||
# will add the scope to protect against a FK violation.
|
||||
source_user.submissions.where(assignment_id: Assignment.where(context_id: enrollments.map(&:course_id))).
|
||||
where.not(assignment_id: user.all_submissions.select(:assignment_id)).
|
||||
update_all(user_id: user.id)
|
||||
source_user.quiz_submissions.where(quiz_id: Quizzes::Quiz.where(context_id: enrollments.map(&:course_id))).
|
||||
where.not(quiz_id: user.quiz_submissions.select(:quiz_id)).
|
||||
update_all(user_id: user.id)
|
||||
end
|
||||
|
||||
def handle_submissions(source_user, user, records)
|
||||
[[:submissions, 'fk_rails_8d85741475'],
|
||||
[:'quizzes/quiz_submissions', 'fk_rails_04850db4b4']].each do |table, foreign_key|
|
||||
model = table.to_s.classify.constantize
|
||||
|
||||
ids_by_shard = records.where(context_type: model.to_s, previous_user_id: user).pluck(:context_id).group_by{|id| Shard.shard_for(id)}
|
||||
other_ids_by_shard = records.where(context_type: model.to_s, previous_user_id: source_user).pluck(:context_id).group_by{|id| Shard.shard_for(id)}
|
||||
|
||||
(ids_by_shard.keys + other_ids_by_shard.keys).uniq.each do |shard|
|
||||
ids = ids_by_shard[shard] || []
|
||||
other_ids = other_ids_by_shard[shard] || []
|
||||
shard.activate do
|
||||
model.transaction do
|
||||
# there is a unique index on assignment_id and user_id or quiz_id
|
||||
# and user_id. Unique indexes are checked after every row during
|
||||
# an update statement to get around this and to allow us to swap
|
||||
# we are setting the user_id to the negative user_id and then back
|
||||
# to the user_id after the conflicting rows have been updated.
|
||||
model.connection.execute("SET CONSTRAINTS #{model.connection.quote_table_name(foreign_key)} DEFERRED")
|
||||
model.where(id: ids).update_all(user_id: -user.id)
|
||||
model.where(id: other_ids).update_all(user_id: source_user.id)
|
||||
model.where(id: ids).update_all(user_id: user.id)
|
||||
end
|
||||
Enrollment.send_later(:recompute_due_dates_and_scores, source_user.id)
|
||||
Enrollment.send_later(:recompute_due_dates_and_scores, user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def restore_workflow_states_from_records(records)
|
||||
records.each do |r|
|
||||
c = r.context
|
||||
next unless c && c.class.columns_hash.key?('workflow_state')
|
||||
c.workflow_state = r.previous_workflow_state unless c.class == Attachment
|
||||
c.file_state = r.previous_workflow_state if c.class == Attachment
|
||||
c.save! if c.changed? && c.valid?
|
||||
def split_users
|
||||
source_user.shard.activate do
|
||||
ActiveRecord::Base.transaction do
|
||||
@restored_user = User.find(merge_data.from_user_id)
|
||||
records = merge_data.records
|
||||
pseudonyms = restore_users
|
||||
records = check_and_update_local_ids(records) if merge_data.from_user_id > Shard::IDS_PER_SHARD
|
||||
records = records.preload(:context)
|
||||
restore_merge_items
|
||||
move_records_to_old_user(records, pseudonyms)
|
||||
# update account associations for each split out user
|
||||
users = [restored_user, source_user]
|
||||
User.update_account_associations(users, all_shards: (restored_user.shard != source_user.shard))
|
||||
merge_data.destroy
|
||||
User.where(id: users).touch_all
|
||||
users
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def restore_merge_items
|
||||
Shard.with_each_shard(restored_user.associated_shards + restored_user.associated_shards(:weak) + restored_user.associated_shards(:shadow)) do
|
||||
UserPastLtiIds.where(user: restored_user, user_lti_id: restored_user.lti_id).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def check_and_update_local_ids(records)
|
||||
if records.where("previous_user_id<?", Shard::IDS_PER_SHARD).where(previous_user_id: restored_user.local_id).exists?
|
||||
records.where(previous_user_id: restored_user.local_id).update_all(previous_user_id: restored_user.global_id)
|
||||
end
|
||||
records.reload
|
||||
end
|
||||
|
||||
def move_records_to_old_user(records, pseudonyms)
|
||||
fix_communication_channels(records.where(context_type: 'CommunicationChannel'))
|
||||
move_user_observers(records.where(context_type: ['UserObserver', 'UserObservationLink'], previous_user_id: restored_user))
|
||||
move_attachments(records.where(context_type: 'Attachment'))
|
||||
enrollment_ids = records.where(context_type: 'Enrollment', previous_user_id: restored_user).pluck(:context_id)
|
||||
Shard.partition_by_shard(enrollment_ids) do |enrollments|
|
||||
restore_enrollments(enrollments)
|
||||
end
|
||||
Shard.partition_by_shard(pseudonyms) do |pseudonyms|
|
||||
move_new_enrollments(enrollment_ids, pseudonyms)
|
||||
end
|
||||
handle_submissions(records)
|
||||
account_users_ids = records.where(context_type: 'AccountUser').pluck(:context_id)
|
||||
AccountUser.where(id: account_users_ids).update_all(user_id: restored_user.id)
|
||||
restore_workflow_states_from_records(records)
|
||||
end
|
||||
|
||||
def restore_enrollments(enrollments)
|
||||
enrollments = Enrollment.where(id: enrollments).where.not(user: restored_user)
|
||||
move_enrollments(enrollments)
|
||||
end
|
||||
|
||||
def move_new_enrollments(enrollment_ids, pseudonyms)
|
||||
new_enrollments = Enrollment.where.not(id: enrollment_ids, user: restored_user).
|
||||
where(sis_pseudonym_id: pseudonyms).shard(pseudonyms.first.shard)
|
||||
move_enrollments(new_enrollments)
|
||||
end
|
||||
|
||||
def move_enrollments(enrollments)
|
||||
enrollments_to_update = filter_enrollments(enrollments)
|
||||
Enrollment.where(id: enrollments_to_update).update_all(user_id: restored_user.id, updated_at: Time.now.utc)
|
||||
courses = enrollments_to_update.map(&:course_id)
|
||||
transfer_enrollment_data(Course.where(id: courses))
|
||||
move_submissions(enrollments_to_update)
|
||||
end
|
||||
|
||||
def filter_enrollments(enrollments)
|
||||
enrollments.reject do |e|
|
||||
# skip conflicting enrollments
|
||||
Enrollment.where(user_id: restored_user,
|
||||
course_section_id: e.course_section_id,
|
||||
type: e.type,
|
||||
role_id: e.role_id).where.not(id: e).shard(e.shard).exists?
|
||||
end
|
||||
end
|
||||
|
||||
def fix_communication_channels(cc_records)
|
||||
if source_user.shard != restored_user.shard
|
||||
source_user.shard.activate do
|
||||
# remove communication channels that didn't exist prior to the merge
|
||||
ccs = CommunicationChannel.where(id: cc_records.where(previous_workflow_state: 'non_existent').pluck(:context_id))
|
||||
DelayedMessage.where(communication_channel_id: ccs).delete_all
|
||||
NotificationPolicy.where(communication_channel: ccs).delete_all
|
||||
ccs.delete_all
|
||||
end
|
||||
end
|
||||
# move moved communication channels back
|
||||
max_position = restored_user.communication_channels.last.try(:position) || 0
|
||||
scope = source_user.communication_channels.where(id: cc_records.where(previous_user_id: restored_user).pluck(:context_id))
|
||||
scope.update_all(["user_id=?, position=position+?", restored_user.id, max_position]) unless scope.empty?
|
||||
|
||||
cc_records.where.not(previous_workflow_state: 'non existent').each do |cr|
|
||||
CommunicationChannel.where(id: cr.context_id).update_all(workflow_state: cr.previous_workflow_state)
|
||||
end
|
||||
end
|
||||
|
||||
def move_user_observers(records)
|
||||
# skip when the user observer is between the two users. Just undlete the record
|
||||
not_obs = UserObservationLink.where(user_id: [source_user, restored_user], observer_id: [source_user, restored_user])
|
||||
obs = UserObservationLink.where(id: records.pluck(:context_id)).where.not(id: not_obs)
|
||||
|
||||
source_user.as_student_observation_links.where(id: obs).update_all(user_id: restored_user.id)
|
||||
source_user.as_observer_observation_links.where(id: obs).update_all(observer_id: restored_user.id)
|
||||
end
|
||||
|
||||
def move_attachments(records)
|
||||
attachments = source_user.attachments.where(id: records.pluck(:context_id))
|
||||
Attachment.migrate_attachments(source_user, restored_user, attachments)
|
||||
end
|
||||
|
||||
def restore_users
|
||||
restore_source_user
|
||||
pseudonyms_ids = merge_data.records.where(context_type: 'Pseudonym').pluck(:context_id)
|
||||
pseudonyms = Pseudonym.where(id: pseudonyms_ids)
|
||||
# the where.not needs to be used incase that user is actually deleted.
|
||||
name = merge_data.items.where.not(user_id: source_user).where(item_type: 'user_name').take&.item || pseudonyms.first.unique_id
|
||||
prefs = merge_data.items.where.not(user_id: source_user).where(item_type: 'user_preferences').take&.item
|
||||
@restored_user ||= User.new
|
||||
@restored_user.name = name
|
||||
@restored_user.preferences = prefs
|
||||
@restored_user.workflow_state = 'registered'
|
||||
shard = Shard.shard_for(merge_data.from_user_id)
|
||||
shard ||= source_user.shard
|
||||
@restored_user.shard = shard if @restored_user.new_record?
|
||||
@restored_user.save!
|
||||
move_pseudonyms_to_user(pseudonyms)
|
||||
pseudonyms
|
||||
end
|
||||
|
||||
def restore_source_user
|
||||
[:avatar_image_source, :avatar_image_url, :avatar_image_updated_at, :avatar_state].each do |attr|
|
||||
avatar_item = merge_data.items.where.not(user_id: source_user).where(item_type: attr).take&.item
|
||||
# we only move avatar items if there were no avatar on the source_user,
|
||||
# so now we only restore it if they match what was on the from_user.
|
||||
source_user[attr] = avatar_item if source_user[attr] == avatar_item
|
||||
end
|
||||
source_user.name = merge_data.items.where(user_id: source_user, item_type: 'user_name').take&.item
|
||||
# we will leave the merged preferences on the user, most of them are for a
|
||||
# specific context that will not be there, but it will keep new
|
||||
# preferences except for terms_of_use.
|
||||
source_user.preferences[:accepted_terms] = merge_data.items.
|
||||
where(user_id: source_user).where(item_type: 'user_preferences').take&.item&.dig(:accepted_terms)
|
||||
source_user.preferences = {} if source_user.preferences == {accepted_terms: nil}
|
||||
source_user.save! if source_user.changed?
|
||||
end
|
||||
|
||||
def move_pseudonyms_to_user(pseudonyms)
|
||||
pseudonyms.each do |pseudonym|
|
||||
pseudonym.update_attribute(:user_id, restored_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
def transfer_enrollment_data(courses)
|
||||
# use a partition proc so that we only run on the actual course shard, not all
|
||||
# shards associated with the course
|
||||
Shard.partition_by_shard(courses, ->(course) {course.shard}) do |shard_course|
|
||||
source_user_id = source_user.id
|
||||
target_user_id = restored_user.id
|
||||
ENROLLMENT_DATA_UPDATES.each do |update|
|
||||
relation = update[:table].classify.constantize.all
|
||||
relation = relation.instance_exec(&update[:scope]) if update[:scope]
|
||||
|
||||
relation.
|
||||
where((update[:context_id] || :context_id) => shard_course,
|
||||
(update[:foreign_key] || :user_id) => source_user_id).
|
||||
update_all((update[:foreign_key] || :user_id) => target_user_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# enrollments are enrollments that have been created since the merge event,
|
||||
# but for a pseudonym that was moved back to the old user.
|
||||
# Also work that has happened since the merge event should moved if the
|
||||
# enrollment is moved.
|
||||
def move_submissions(enrollments)
|
||||
# there should be no conflicts here because this is only called for
|
||||
# enrollments that were updated which already excluded conflicts, but we
|
||||
# will add the scope to protect against a FK violation.
|
||||
source_user.submissions.where(assignment_id: Assignment.where(context_id: enrollments.map(&:course_id))).
|
||||
where.not(assignment_id: restored_user.all_submissions.select(:assignment_id)).
|
||||
update_all(user_id: restored_user.id)
|
||||
source_user.quiz_submissions.where(quiz_id: Quizzes::Quiz.where(context_id: enrollments.map(&:course_id))).
|
||||
where.not(quiz_id: restored_user.quiz_submissions.select(:quiz_id)).
|
||||
update_all(user_id: restored_user.id)
|
||||
end
|
||||
|
||||
def handle_submissions(records)
|
||||
[[:submissions, 'fk_rails_8d85741475'],
|
||||
[:'quizzes/quiz_submissions', 'fk_rails_04850db4b4']].each do |table, foreign_key|
|
||||
model = table.to_s.classify.constantize
|
||||
|
||||
ids_by_shard = records.where(context_type: model.to_s, previous_user_id: restored_user).pluck(:context_id).group_by {|id| Shard.shard_for(id)}
|
||||
other_ids_by_shard = records.where(context_type: model.to_s, previous_user_id: source_user).pluck(:context_id).group_by {|id| Shard.shard_for(id)}
|
||||
|
||||
(ids_by_shard.keys + other_ids_by_shard.keys).uniq.each do |shard|
|
||||
ids = ids_by_shard[shard] || []
|
||||
other_ids = other_ids_by_shard[shard] || []
|
||||
shard.activate do
|
||||
model.transaction do
|
||||
# there is a unique index on assignment_id and user_id or quiz_id
|
||||
# and user_id. Unique indexes are checked after every row during
|
||||
# an update statement to get around this and to allow us to swap
|
||||
# we are setting the user_id to the negative user_id and then back
|
||||
# to the user_id after the conflicting rows have been updated.
|
||||
model.connection.execute("SET CONSTRAINTS #{model.connection.quote_table_name(foreign_key)} DEFERRED")
|
||||
model.where(id: ids).update_all(user_id: -restored_user.id)
|
||||
model.where(id: other_ids).update_all(user_id: source_user.id)
|
||||
model.where(id: ids).update_all(user_id: restored_user.id)
|
||||
end
|
||||
Enrollment.send_later(:recompute_due_dates_and_scores, source_user.id)
|
||||
Enrollment.send_later(:recompute_due_dates_and_scores, restored_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def restore_workflow_states_from_records(records)
|
||||
records.each do |r|
|
||||
c = r.context
|
||||
next unless c && c.class.columns_hash.key?('workflow_state')
|
||||
c.workflow_state = r.previous_workflow_state unless c.class == Attachment
|
||||
c.file_state = r.previous_workflow_state if c.class == Attachment
|
||||
c.save! if c.changed? && c.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
class UserMergeData < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :from_user, class_name: 'User'
|
||||
has_many :user_merge_data_records
|
||||
has_many :records, class_name: 'UserMergeDataRecord', inverse_of: :merge_data
|
||||
has_many :items, class_name: 'UserMergeDataItem', inverse_of: :merge_data
|
||||
|
||||
scope :active, -> { where.not(workflow_state: 'deleted') }
|
||||
scope :splitable, -> { where('created_at > ?', split_time) }
|
||||
|
@ -37,7 +38,7 @@ class UserMergeData < ActiveRecord::Base
|
|||
self.shard.activate do
|
||||
objects.each do |o|
|
||||
user ||= o.user_id
|
||||
r = self.user_merge_data_records.new(context: o, previous_user_id: user)
|
||||
r = self.records.new(context: o, previous_user_id: user)
|
||||
r.previous_workflow_state = o.workflow_state if o.class.columns_hash.key?('workflow_state')
|
||||
r.previous_workflow_state = o.file_state if o.class == Attachment
|
||||
r.previous_workflow_state = workflow_state if workflow_state
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
#
|
||||
# Copyright (C) 2019 - present 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 UserMergeDataItem < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :merge_data, class_name: 'UserMergeData', inverse_of: :items
|
||||
|
||||
serialize :item
|
||||
end
|
|
@ -17,7 +17,7 @@
|
|||
#
|
||||
class UserMergeDataRecord < ActiveRecord::Base
|
||||
belongs_to :previous_user, class_name: 'User'
|
||||
belongs_to :user_merge_data
|
||||
belongs_to :merge_data, class_name: 'UserMergeData', inverse_of: :records
|
||||
belongs_to :context, polymorphic: [:account_user, :enrollment, :pseudonym, :user_observer, :user_observation_link,
|
||||
:attachment, :communication_channel, :user_service,
|
||||
:submission, {quiz_submission: 'Quizzes::QuizSubmission'}]
|
||||
|
|
|
@ -22,7 +22,8 @@ class RecomputeMergedEnrollments < ActiveRecord::Migration[4.2]
|
|||
def up
|
||||
start_date = DateTime.parse("2016-08-05")
|
||||
merged_enrollment_ids = UserMergeDataRecord.where(:context_type => "Enrollment").
|
||||
joins(:user_merge_data).where("user_merge_data.updated_at > ?", start_date).pluck(:context_id)
|
||||
joins("INNER JOIN #{UserMergeData.quoted_table_name} ON user_merge_data_records.user_merge_data_id = user_merge_data.id").
|
||||
where("user_merge_data.updated_at > ?", start_date).pluck(:context_id)
|
||||
|
||||
if merged_enrollment_ids.any?
|
||||
Shard.partition_by_shard(merged_enrollment_ids) do |sliced_ids|
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
#
|
||||
# Copyright (C) 2019 - present 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 UserMergeDataItems < ActiveRecord::Migration[5.1]
|
||||
tag :predeploy
|
||||
|
||||
def change
|
||||
create_table :user_merge_data_items do |t|
|
||||
t.references :user_merge_data, limit: 8, foreign_key: true, index: true, null: false
|
||||
t.references :user, limit: 8, foreign_key: true, index: true, null: false
|
||||
t.string :item_type, null: false, limit: 255
|
||||
t.text :item, null: false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,29 +22,45 @@ class UserMerge
|
|||
end
|
||||
|
||||
attr_reader :from_user
|
||||
attr_accessor :data
|
||||
attr_accessor :target_user, :data, :merge_data
|
||||
|
||||
def initialize(from_user)
|
||||
@from_user = from_user
|
||||
@target_user = nil
|
||||
@merge_data = nil
|
||||
@data = []
|
||||
end
|
||||
|
||||
def into(target_user)
|
||||
return unless target_user
|
||||
return if target_user == from_user
|
||||
@target_user = target_user
|
||||
target_user.associate_with_shard(from_user.shard, :shadow)
|
||||
# we also store records for the from_user on the target shard for a split
|
||||
from_user.associate_with_shard(target_user.shard, :shadow)
|
||||
user_merge_data = target_user.shard.activate do
|
||||
UserMergeData.create!(user: target_user, from_user: from_user)
|
||||
end
|
||||
target_user.shard.activate do
|
||||
@merge_data = UserMergeData.create!(user: target_user, from_user: from_user)
|
||||
|
||||
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|
|
||||
target_user[attr] = from_user[attr]
|
||||
items = []
|
||||
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|
|
||||
items << merge_data.items.new(user: from_user, item_type: attr.to_s, item: from_user[attr]) if from_user[attr]
|
||||
target_user[attr] = from_user[attr]
|
||||
end
|
||||
end
|
||||
|
||||
# record the users names and preferences in case of split.
|
||||
items << merge_data.items.new(user: from_user, item_type: 'user_name', item: from_user.name)
|
||||
items << merge_data.items.new(user: target_user, item_type: 'user_name', item: target_user.name)
|
||||
UserMergeDataItem.bulk_insert_objects(items)
|
||||
|
||||
# bulk insert doesn't play nice with the hash values of preferences.
|
||||
merge_data.items.create!(user: from_user, item_type: 'user_preferences', item: from_user.preferences)
|
||||
merge_data.items.create!(user: target_user, item_type: 'user_preferences', item: target_user.preferences)
|
||||
|
||||
target_user.preferences = target_user.preferences.merge(from_user.preferences)
|
||||
target_user.save if target_user.changed?
|
||||
end
|
||||
target_user.save if target_user.changed?
|
||||
|
||||
[:strong, :weak, :shadow].each do |strength|
|
||||
from_user.associated_shards(strength).each do |shard|
|
||||
|
@ -52,25 +68,22 @@ class UserMerge
|
|||
end
|
||||
end
|
||||
|
||||
populate_past_lti_ids(target_user)
|
||||
|
||||
handle_communication_channels(target_user, user_merge_data)
|
||||
|
||||
destroy_conflicting_module_progressions(@from_user, target_user)
|
||||
|
||||
move_enrollments(target_user, user_merge_data)
|
||||
populate_past_lti_ids
|
||||
handle_communication_channels
|
||||
destroy_conflicting_module_progressions
|
||||
move_enrollments
|
||||
|
||||
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
|
||||
pseudonyms_to_move = Pseudonym.where(user_id: from_user)
|
||||
user_merge_data.add_more_data(pseudonyms_to_move)
|
||||
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)
|
||||
end
|
||||
|
||||
handle_submissions(target_user, user_merge_data)
|
||||
handle_submissions
|
||||
|
||||
from_user.all_conversations.find_each { |c| c.move_to_user(target_user) }
|
||||
|
||||
|
@ -86,11 +99,11 @@ class UserMerge
|
|||
end
|
||||
|
||||
account_users = AccountUser.where(user_id: from_user)
|
||||
user_merge_data.add_more_data(account_users)
|
||||
merge_data.add_more_data(account_users)
|
||||
account_users.update_all(user_id: target_user.id)
|
||||
|
||||
attachments = Attachment.where(user_id: from_user)
|
||||
user_merge_data.add_more_data(attachments)
|
||||
merge_data.add_more_data(attachments)
|
||||
Attachment.send_later(:migrate_attachments, from_user, target_user)
|
||||
|
||||
updates = {}
|
||||
|
@ -114,7 +127,7 @@ class UserMerge
|
|||
scope = klass.where(column => from_user)
|
||||
klass.transaction do
|
||||
if version_updates.include?(table)
|
||||
update_versions(from_user, target_user, scope, table, column)
|
||||
update_versions(scope, table, column)
|
||||
end
|
||||
scope.update_all(column => target_user.id)
|
||||
end
|
||||
|
@ -131,7 +144,7 @@ class UserMerge
|
|||
update_all(context_id: target_user.id, context_code: target_user.asset_string)
|
||||
end
|
||||
|
||||
move_observees(target_user, user_merge_data)
|
||||
move_observees
|
||||
|
||||
Enrollment.send_later(:recompute_due_dates_and_scores, target_user.id)
|
||||
target_user.update_account_associations
|
||||
|
@ -142,7 +155,7 @@ class UserMerge
|
|||
from_user.destroy
|
||||
end
|
||||
|
||||
def populate_past_lti_ids(target_user)
|
||||
def populate_past_lti_ids
|
||||
Shard.with_each_shard(from_user.associated_shards + from_user.associated_shards(:weak) + from_user.associated_shards(:shadow)) do
|
||||
lti_ids = []
|
||||
{enrollments: :course, group_memberships: :group, account_users: :account}.each do |klass, type|
|
||||
|
@ -160,62 +173,62 @@ class UserMerge
|
|||
end
|
||||
end
|
||||
|
||||
def handle_communication_channels(target_user, user_merge_data)
|
||||
def handle_communication_channels
|
||||
max_position = target_user.communication_channels.last.try(:position) || 0
|
||||
to_retire_ids = []
|
||||
known_ccs = target_user.communication_channels.pluck(:id)
|
||||
from_user.communication_channels.each do |cc|
|
||||
# have to find conflicting CCs, and make sure we don't have conflicts
|
||||
target_cc = detect_conflicting_cc(cc, target_user)
|
||||
target_cc = detect_conflicting_cc(cc)
|
||||
|
||||
if !target_cc && from_user.shard != target_user.shard
|
||||
User.clone_communication_channel(cc, target_user, max_position)
|
||||
new_cc = target_user.communication_channels.where.not(id: known_ccs).take
|
||||
known_ccs << new_cc.id
|
||||
user_merge_data.build_more_data([new_cc], user: target_user, workflow_state: 'non_existent', data: data)
|
||||
merge_data.build_more_data([new_cc], user: target_user, workflow_state: 'non_existent', data: data)
|
||||
end
|
||||
|
||||
next unless target_cc
|
||||
to_retire = handle_conflicting_ccs(cc, target_cc, target_user, max_position)
|
||||
to_retire = handle_conflicting_ccs(cc, target_cc, max_position)
|
||||
if to_retire
|
||||
keeper = ([target_cc, cc] - [to_retire]).first
|
||||
copy_notificaion_policies(to_retire, keeper, user_merge_data)
|
||||
copy_notificaion_policies(to_retire, keeper)
|
||||
to_retire_ids << to_retire.id
|
||||
end
|
||||
end
|
||||
|
||||
finish_ccs(max_position, target_user, to_retire_ids, user_merge_data)
|
||||
finish_ccs(max_position, to_retire_ids)
|
||||
end
|
||||
|
||||
def detect_conflicting_cc(source_cc, target_user)
|
||||
def detect_conflicting_cc(source_cc)
|
||||
target_user.communication_channels.detect do |c|
|
||||
c.path.downcase == source_cc.path.downcase && c.path_type == source_cc.path_type
|
||||
end
|
||||
end
|
||||
|
||||
def finish_ccs(max_position, target_user, to_retire_ids, user_merge_data)
|
||||
def finish_ccs(max_position, to_retire_ids)
|
||||
if from_user.shard != target_user.shard
|
||||
handle_cross_shard_cc(target_user, user_merge_data)
|
||||
handle_cross_shard_cc
|
||||
else
|
||||
from_user.shard.activate do
|
||||
ccs = CommunicationChannel.where(id: to_retire_ids).where.not(workflow_state: 'retired')
|
||||
user_merge_data.build_more_data(ccs, data: data) unless to_retire_ids.empty?
|
||||
merge_data.build_more_data(ccs, data: data) unless to_retire_ids.empty?
|
||||
ccs.update_all(workflow_state: 'retired') unless to_retire_ids.empty?
|
||||
end
|
||||
scope = from_user.communication_channels.where.not(workflow_state: 'retired')
|
||||
scope = scope.where.not(id: to_retire_ids) unless to_retire_ids.empty?
|
||||
unless scope.empty?
|
||||
user_merge_data.build_more_data(scope, data: data)
|
||||
merge_data.build_more_data(scope, data: data)
|
||||
scope.update_all(["user_id=?, position=position+?", target_user, max_position])
|
||||
end
|
||||
end
|
||||
user_merge_data.bulk_insert_merge_data(data)
|
||||
merge_data.bulk_insert_merge_data(data)
|
||||
@data = []
|
||||
end
|
||||
|
||||
def handle_cross_shard_cc(target_user, user_merge_data)
|
||||
def handle_cross_shard_cc
|
||||
ccs = from_user.communication_channels.where.not(workflow_state: 'retired')
|
||||
user_merge_data.build_more_data(ccs, data: data) unless ccs.empty?
|
||||
merge_data.build_more_data(ccs, data: data) unless ccs.empty?
|
||||
ccs.update_all(workflow_state: 'retired') unless ccs.empty?
|
||||
|
||||
from_user.user_services.each do |us|
|
||||
|
@ -223,13 +236,13 @@ class UserMerge
|
|||
new_us.shard = target_user.shard
|
||||
new_us.user = target_user
|
||||
new_us.save!
|
||||
user_merge_data.build_more_data([new_us], user: target_user, workflow_state: 'non_existent', data: data)
|
||||
merge_data.build_more_data([new_us], user: target_user, workflow_state: 'non_existent', data: data)
|
||||
end
|
||||
user_merge_data.build_more_data(from_user.user_services, data: data)
|
||||
merge_data.build_more_data(from_user.user_services, data: data)
|
||||
from_user.user_services.delete_all
|
||||
end
|
||||
|
||||
def handle_conflicting_ccs(source_cc, target_cc, target_user, max_position)
|
||||
def handle_conflicting_ccs(source_cc, target_cc, max_position)
|
||||
# we prefer keeping the "most" active one, preferring the target user if they're equal
|
||||
# the comments inline show all the different cases, with the source cc on the left,
|
||||
# target cc on the right. The * indicates the CC that will be retired in order
|
||||
|
@ -265,7 +278,7 @@ class UserMerge
|
|||
to_retire
|
||||
end
|
||||
|
||||
def copy_notificaion_policies(to_retire, keeper, user_merge_data)
|
||||
def copy_notificaion_policies(to_retire, keeper)
|
||||
# cross shard channels get cloned and so do notification_policies
|
||||
return unless to_retire.shard == keeper.shard
|
||||
# if the communication_channel is already retired, don't bother.
|
||||
|
@ -282,15 +295,15 @@ class UserMerge
|
|||
NotificationPolicy.bulk_insert_objects(new_nps)
|
||||
end
|
||||
|
||||
def move_observees(target_user, user_merge_data)
|
||||
def move_observees
|
||||
# record all the records before destroying them
|
||||
# pass the from_user since user_id will be the observer
|
||||
user_merge_data.build_more_data(from_user.as_observer_observation_links, user: from_user, data: data)
|
||||
user_merge_data.build_more_data(from_user.as_student_observation_links, data: data)
|
||||
merge_data.build_more_data(from_user.as_observer_observation_links, user: from_user, data: data)
|
||||
merge_data.build_more_data(from_user.as_student_observation_links, data: data)
|
||||
# delete duplicate or invalid observers/observees, move the rest
|
||||
from_user.as_observer_observation_links.where(user_id: target_user.as_observer_observation_links.map(&:user_id)).destroy_all
|
||||
from_user.as_observer_observation_links.where(user_id: target_user).destroy_all
|
||||
user_merge_data.add_more_data(target_user.as_observer_observation_links.where(user_id: from_user), user: target_user, data: data)
|
||||
merge_data.add_more_data(target_user.as_observer_observation_links.where(user_id: from_user), user: target_user, data: data)
|
||||
@data = []
|
||||
target_user.as_observer_observation_links.where(user_id: from_user).destroy_all
|
||||
target_user.associate_with_shard(from_user.shard) if from_user.as_observer_observation_links.exists?
|
||||
|
@ -304,7 +317,7 @@ class UserMerge
|
|||
target_user.as_student_observation_links.where(observer_id: xor_observer_ids).each(&:create_linked_enrollments)
|
||||
end
|
||||
|
||||
def destroy_conflicting_module_progressions(from_user, target_user)
|
||||
def destroy_conflicting_module_progressions
|
||||
# there is a unique index on the context_module_progressions table
|
||||
# we need to delete all the conflicting context_module_progressions
|
||||
# without impacting the users module progress and without having to
|
||||
|
@ -374,7 +387,7 @@ class UserMerge
|
|||
keeper.destroy
|
||||
end
|
||||
|
||||
def handle_conflicts(column, target_user, user_merge_data)
|
||||
def handle_conflicts(column)
|
||||
users = [from_user, target_user]
|
||||
|
||||
# get each pair of conflicts and "handle them"
|
||||
|
@ -391,49 +404,49 @@ class UserMerge
|
|||
# both target and from users records will be recorded in case of a split.
|
||||
if to_update.exists?
|
||||
# record both records state since both will change
|
||||
user_merge_data.build_more_data(scope, data: data)
|
||||
merge_data.build_more_data(scope, data: data)
|
||||
update_enrollment_state(scope, keeper)
|
||||
end
|
||||
|
||||
# identify if the from users records are worse states than target user
|
||||
to_delete = scope.active.where.not(id: keeper).where(column => from_user)
|
||||
# record the current state in case of split
|
||||
user_merge_data.build_more_data(to_delete, data: data)
|
||||
merge_data.build_more_data(to_delete, data: data)
|
||||
# mark all conflicts on from_user as deleted so they will not be moved later
|
||||
to_delete.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
def remove_self_observers(target_user, user_merge_data)
|
||||
def remove_self_observers
|
||||
# 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.build_more_data(to_delete, data: data)
|
||||
merge_data.build_more_data(to_delete, data: data)
|
||||
to_delete.destroy_all
|
||||
end
|
||||
|
||||
def move_enrollments(target_user, user_merge_data)
|
||||
def move_enrollments
|
||||
[:associated_user_id, :user_id].each do |column|
|
||||
Shard.with_each_shard(from_user.associated_shards) do
|
||||
Enrollment.transaction do
|
||||
handle_conflicts(column, target_user, user_merge_data)
|
||||
remove_self_observers(target_user, user_merge_data)
|
||||
handle_conflicts(column)
|
||||
remove_self_observers
|
||||
# move all the enrollments that have not been marked as deleted to the target user
|
||||
to_move = Enrollment.active.where(column => from_user)
|
||||
# upgrade to strong association if there are any enrollments
|
||||
target_user.associate_with_shard(from_user.shard) if to_move.exists?
|
||||
user_merge_data.build_more_data(to_move, data: data)
|
||||
merge_data.build_more_data(to_move, data: data)
|
||||
to_move.update_all(column => target_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
user_merge_data.bulk_insert_merge_data(data)
|
||||
merge_data.bulk_insert_merge_data(data)
|
||||
@data = []
|
||||
end
|
||||
|
||||
def handle_submissions(target_user, user_merge_data)
|
||||
def handle_submissions
|
||||
[
|
||||
[:assignment_id, :submissions],
|
||||
[:quiz_id, :'quizzes/quiz_submissions']
|
||||
|
@ -457,9 +470,9 @@ class UserMerge
|
|||
to_move_ids += scope.having_submission.select(unique_id).where.not(unique_id => already_scope.having_submission.select(unique_id), id: to_move_ids).pluck(:id)
|
||||
to_move = scope.where(id: to_move_ids).to_a
|
||||
move_back = already_scope.where(unique_id => to_move.map(&unique_id)).to_a
|
||||
user_merge_data.build_more_data(to_move, data: data)
|
||||
user_merge_data.build_more_data(move_back, data: data)
|
||||
swap_submission(model, move_back, table, target_user, to_move, to_move_ids, 'fk_rails_8d85741475')
|
||||
merge_data.build_more_data(to_move, data: data)
|
||||
merge_data.build_more_data(move_back, data: data)
|
||||
swap_submission(model, move_back, table, to_move, to_move_ids, 'fk_rails_8d85741475')
|
||||
elsif model.name == "Quizzes::QuizSubmission"
|
||||
subscope = already_scope.to_a
|
||||
to_move = model.where(user_id: from_user).joins(:submission).where(submissions: {user_id: target_user}).to_a
|
||||
|
@ -467,19 +480,19 @@ class UserMerge
|
|||
|
||||
to_move += scope.where("#{unique_id} NOT IN (?)", [subscope.map(&unique_id), move_back.map(&unique_id)].flatten).to_a
|
||||
move_back += already_scope.where(unique_id => to_move.map(&unique_id)).to_a
|
||||
user_merge_data.build_more_data(to_move, data: data)
|
||||
user_merge_data.build_more_data(move_back, data: data)
|
||||
swap_submission(model, move_back, table, target_user, to_move, to_move, 'fk_rails_04850db4b4')
|
||||
merge_data.build_more_data(to_move, data: data)
|
||||
merge_data.build_more_data(move_back, data: data)
|
||||
swap_submission(model, move_back, table, to_move, to_move, 'fk_rails_04850db4b4')
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "migrating #{table} column user_id failed: #{e}"
|
||||
end
|
||||
end
|
||||
user_merge_data.bulk_insert_merge_data(data)
|
||||
merge_data.bulk_insert_merge_data(data)
|
||||
@data = []
|
||||
end
|
||||
|
||||
def swap_submission(model, move_back, table, target_user, to_move, to_move_ids, fk)
|
||||
def swap_submission(model, move_back, table, to_move, to_move_ids, fk)
|
||||
model.transaction do
|
||||
# there is a unique index on assignment_id and user_id. Unique
|
||||
# indexes are checked after every row during an update statement
|
||||
|
@ -491,12 +504,12 @@ class UserMerge
|
|||
model.where(id: move_back).update_all(user_id: -from_user.id) if target_user.shard == from_user.shard
|
||||
model.where(id: to_move_ids).update_all(user_id: target_user.id)
|
||||
model.where(id: move_back).update_all(user_id: from_user.id)
|
||||
update_versions(from_user, target_user, model.where(id: to_move), table, :user_id)
|
||||
update_versions(target_user, from_user, model.where(id: move_back), table, :user_id)
|
||||
update_versions(model.where(id: to_move), table, :user_id)
|
||||
update_versions(model.where(id: move_back), table, :user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def update_versions(from_user, target_user, scope, table, column)
|
||||
def update_versions(scope, table, column)
|
||||
scope.find_ids_in_batches do |ids|
|
||||
versionable_type = table.to_s.classify
|
||||
# TODO: This is a hack to support namespacing
|
||||
|
|
|
@ -38,7 +38,7 @@ describe UserMerge do
|
|||
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
|
||||
expect(merge_data.records.pluck(:context_id).sort).to eq [pseudonym.id, pseudonym2.id].sort
|
||||
user2.reload
|
||||
expect(user2.pseudonyms).to be_empty
|
||||
user1.reload
|
||||
|
@ -60,7 +60,7 @@ describe UserMerge do
|
|||
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
|
||||
expect(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
|
||||
|
@ -237,7 +237,7 @@ describe UserMerge do
|
|||
UserMerge.from(user1).into(user2)
|
||||
user1.reload
|
||||
user2.reload
|
||||
records = UserMergeData.where(user_id: user2).take.user_merge_data_records
|
||||
records = UserMergeData.where(user_id: user2).take.records
|
||||
expect(records.count).to eq 8
|
||||
record = records.where(context_id: cc1).take
|
||||
expect(record.previous_user_id).to eq user1.id
|
||||
|
@ -286,12 +286,12 @@ describe UserMerge do
|
|||
|
||||
UserMerge.from(user1).into(user2)
|
||||
merge_data = UserMergeData.where(user_id: user2).first
|
||||
expect(merge_data.user_merge_data_records.pluck(:context_id).sort).
|
||||
expect(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
|
||||
merge_data_record = 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
|
||||
|
@ -312,20 +312,20 @@ describe UserMerge do
|
|||
UserMerge.from(user2).into(user1)
|
||||
merge_data = UserMergeData.where(user_id: user1).first
|
||||
|
||||
expect(merge_data.user_merge_data_records.pluck(:context_id).sort).
|
||||
expect(merge_data.records.pluck(:context_id).sort).
|
||||
to eq [enrollment1.id, enrollment2.id].sort
|
||||
enrollment1.reload
|
||||
expect(enrollment1.user).to eq user1
|
||||
expect(enrollment1.workflow_state).to eq 'active'
|
||||
expect(enrollment1.enrollment_state.state).to eq 'active'
|
||||
merge_data_record = merge_data.user_merge_data_records.where(context_id: enrollment1).first
|
||||
merge_data_record = merge_data.records.where(context_id: enrollment1).first
|
||||
expect(merge_data_record.previous_workflow_state).to eq 'invited'
|
||||
|
||||
enrollment2.reload
|
||||
expect(enrollment2.user).to eq user2
|
||||
expect(enrollment2.workflow_state).to eq 'deleted'
|
||||
expect(enrollment2.enrollment_state.state).to eq 'deleted'
|
||||
merge_data_record2 = merge_data.user_merge_data_records.where(context_id: enrollment2).first
|
||||
merge_data_record2 = merge_data.records.where(context_id: enrollment2).first
|
||||
expect(merge_data_record2.previous_workflow_state).to eq 'active'
|
||||
end
|
||||
|
||||
|
@ -366,7 +366,7 @@ describe UserMerge do
|
|||
|
||||
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
|
||||
o = 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'
|
||||
|
@ -412,7 +412,7 @@ describe UserMerge do
|
|||
|
||||
UserMerge.from(user1).into(user2)
|
||||
data = UserMergeData.where(user_id: user2).first
|
||||
expect(data.user_merge_data_records.where(context_type: 'UserObservationLink').count).to eq 2
|
||||
expect(data.records.where(context_type: 'UserObservationLink').count).to eq 2
|
||||
user1.reload
|
||||
expect(user1.linked_observers).to be_empty
|
||||
expect(UserObservationLink.where(:student => user1).first.workflow_state).to eq 'deleted'
|
||||
|
|
|
@ -28,6 +28,58 @@ describe SplitUsers do
|
|||
let(:account1) { Account.default }
|
||||
let(:sub_account) { account1.sub_accounts.create! }
|
||||
|
||||
it 'should restore terms_of use one way' do
|
||||
user1.accept_terms
|
||||
user1.save!
|
||||
UserMerge.from(user2).into(user1)
|
||||
SplitUsers.split_db_users(user1)
|
||||
expect(user2.reload.preferences[:accepted_terms]).to be_nil
|
||||
expect(user1.reload.preferences[:accepted_terms]).to_not be_nil
|
||||
end
|
||||
|
||||
it 'should restore terms_of use other way' do
|
||||
user1.accept_terms
|
||||
user1.save!
|
||||
UserMerge.from(user1).into(user2)
|
||||
expect(user2.reload.preferences[:accepted_terms]).to_not be_nil
|
||||
SplitUsers.split_db_users(user2)
|
||||
expect(user1.reload.preferences[:accepted_terms]).to_not be_nil
|
||||
expect(user2.reload.preferences[:accepted_terms]).to be_nil
|
||||
end
|
||||
|
||||
it 'should restore terms_of use no way' do
|
||||
UserMerge.from(user1).into(user2)
|
||||
user2.accept_terms
|
||||
user2.save!
|
||||
SplitUsers.split_db_users(user2)
|
||||
expect(user2.reload.preferences[:accepted_terms]).to be_nil
|
||||
expect(user1.reload.preferences[:accepted_terms]).to be_nil
|
||||
end
|
||||
|
||||
it 'should restore terms_of use both ways' do
|
||||
user1.accept_terms
|
||||
user1.save!
|
||||
user2.accept_terms
|
||||
user2.save!
|
||||
UserMerge.from(user1).into(user2)
|
||||
SplitUsers.split_db_users(user2)
|
||||
expect(user2.reload.preferences[:accepted_terms]).to_not be_nil
|
||||
expect(user1.reload.preferences[:accepted_terms]).to_not be_nil
|
||||
end
|
||||
|
||||
it 'should restore names' do
|
||||
user1.name = "jimmy one"
|
||||
user1.save!
|
||||
user2.name = "jenny one"
|
||||
user2.save!
|
||||
UserMerge.from(user1).into(user2)
|
||||
user2.name = "other name"
|
||||
user2.save!
|
||||
SplitUsers.split_db_users(user2)
|
||||
expect(user1.reload.name).to eq "jimmy one"
|
||||
expect(user2.reload.name).to eq "jenny one"
|
||||
end
|
||||
|
||||
it 'should restore pseudonyms to the original user' do
|
||||
pseudonym1 = user1.pseudonyms.create!(unique_id: 'sam1@example.com')
|
||||
pseudonym2 = user2.pseudonyms.create!(unique_id: 'sam2@example.com')
|
||||
|
|
Loading…
Reference in New Issue