Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge users through the console #3327

Merged
merged 22 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class User < ApplicationRecord
has_many :events, dependent: :restrict_with_error
has_many :exports, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :evaluation_users, inverse_of: :user, dependent: :restrict_with_error
has_one :rights_request, dependent: :destroy

has_many :subscribed_courses,
Expand Down Expand Up @@ -290,6 +291,81 @@ def set_search
self.search = "#{username || ''} #{first_name || ''} #{last_name || ''}"
end

def merge_into(other, force: false)
errors.add(:merge, 'User belongs to different institution') if other.institution_id != institution_id && other.institution_id.present? && institution_id.present?
errors.add(:merge, 'User has different permissions') if other.permission != permission && !force
return false if errors.any?

transaction do
jorg-vr marked this conversation as resolved.
Show resolved Hide resolved
other.permission = permission if (permission == 'staff' && other.permission == 'student') \
|| (permission == 'zeus' && other.permission != 'zeus')

other.institution_id = institution_id if other.institution_id.nil?

identities.each do |i|
if other.identities.find { |oi| oi.provider_id == i.provider_id }
i.destroy!
else
i.update!(user: other)
end
end

rights_request.update!(user: other) if !rights_request.nil? && other.permission == 'student' && other.rights_request.nil?

course_memberships.each do |cm|
other_cm = other.course_memberships.find { |ocm| ocm.course_id == cm.course_id }
if other_cm.nil?
cm.update!(user: other)
elsif other_cm.status == cm.status \
|| other_cm.status == 'course_admin' \
|| (other_cm.status == 'student' && cm.status != 'course_admin') \
|| (other_cm.status == 'unsubscribed' && cm.status == 'pending')
other_cm.update!(favorite: true) if cm.favorite
cm.destroy!
else
cm.update!(favorite: true) if other_cm.favorite
other_cm.destroy!
cm.update!(user: other)
end
end

submissions.each { |s| s.update!(user: other) }
api_tokens.each { |at| at.update!(user: other) }
events.each { |e| e.update!(user: other) }
exports.each { |e| e.update!(user: other) }
notifications.each { |n| n.update!(user: other) }
annotations.each { |a| a.update!(user: other, last_updated_by_id: other.id) }
questions.each { |q| q.update!(user: other) }

evaluation_users.each do |eu|
if other.evaluation_users.find { |oeu| oeu.evaluation_id == eu.evaluation_id }
eu.destroy!
else
eu.update!(user: other)
end
end

activity_read_states.each do |ars|
if other.activity_read_states.find { |oars| oars.activity_id == ars.activity_id }
ars.destroy!
else
ars.update!(user: other)
end
end

repository_admins.each do |ra|
if other.repository_admins.find { |ora| ora.repository_id == ra.repository_id }
ra.destroy!
else
ra.update!(user: other)
end
end

reload
destroy!
end
end

private

def set_token
Expand Down
63 changes: 63 additions & 0 deletions lib/tasks/merge_users.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

task :merge_users, [:arg1, :arg2] => :environment do |task, args|
u1_id = args[:arg1].to_i
u2_id = args[:arg2].to_i

MergeUsers.new.merge_users_interactive u1_id, u2_id
end

class MergeUsers
niknetniko marked this conversation as resolved.
Show resolved Hide resolved
LABEL_WIDTH = 20
USER_WIDTH = 50

def initialize(input = $stdin, output = $stdout)
@input = input
@output = output
end

def print_users(*users)
@output.puts 'Username: '.ljust(LABEL_WIDTH) + users.map { |u| u.username.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Email: '.ljust(LABEL_WIDTH) + users.map { |u| u.email.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Name: '.ljust(LABEL_WIDTH) + users.map { |u| u.full_name.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Permission: '.ljust(LABEL_WIDTH) + users.map { |u| u.permission.to_s.ljust(USER_WIDTH) }.join

@output.puts 'Courses: '.ljust(LABEL_WIDTH) + users.map { |u| u.courses.count.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Submissions: '.ljust(LABEL_WIDTH) + users.map { |u| u.submissions.count.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Read states: '.ljust(LABEL_WIDTH) + users.map { |u| u.activity_read_states.count.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Repositories: '.ljust(LABEL_WIDTH) + users.map { |u| u.repositories.count.to_s.ljust(USER_WIDTH) }.join
@output.puts 'Evaluations: '.ljust(LABEL_WIDTH) + users.map { |u| u.evaluation_users.count.to_s.ljust(USER_WIDTH) }.join
end

def merge_users_interactive(u1_id, u2_id)
u1 = User.find(u1_id)
u2 = User.find(u2_id)

print_users u1, u2

@output.puts ''

c = ''
until %W[\r \n y n f].include? c
u1, u2 = u2, u1 if c == 's'
@output.puts "Invalid input #{c}" unless ['s', ''].include?(c)
@output.puts "Are you sure you want to merge #{u1.username} into #{u2.username}? (y)es|(N)o|(f)orce|(s)wap"
c = @input.getch.downcase
end

@output.puts ''

if %W[\r \n n].include? c
@output.puts 'Merge cancelled'
return
end

success = u1.merge_into(u2, force: c == 'f')
if success
@output.puts "Successfully merged #{u1.username} into #{u2.username}"
print_users u2
else
@output.puts 'Merge failed'
@output.puts u1.errors.full_messages.join('\n')
end
end
end
231 changes: 231 additions & 0 deletions test/models/user_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,235 @@ def setup
@user.subscribed_courses.first.update(year: '2')
assert_equal [@user.subscribed_courses.first], @user.drawer_courses
end

test 'user should be removed after merge' do
u1 = create :user
u2 = create :user
u1.merge_into(u2)

assert_not u1.persisted?
end

test 'merge should fail if institutions are different' do
i1 = create :institution
i2 = create :institution
u1 = create :user, institution: i1
u2 = create :user, institution: i2

result = u1.merge_into(u2)

assert_not result
assert u1.persisted?
end

test 'merge should succeed if only one institution is set' do
i1 = create :institution
u1 = create :user, institution: i1
u2 = create :user

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal i1, u2.institution
end

test 'merge should fail if permissions are different' do
u1 = create :user, permission: 'student'
u2 = create :user, permission: 'staff'

result = u1.merge_into(u2)

assert_not result
assert u1.persisted?
end

test 'merge should take highest permission if force is used' do
u1 = create :user, permission: 'zeus'
u2 = create :user, permission: 'staff'

result = u1.merge_into(u2, force: true)

assert result
assert_not u1.persisted?
assert_equal 'zeus', u2.permission
end

test 'merge should transfer all associated objects to the other user' do
u1 = create :user
u2 = create :user

[u1, u2].each do |u|
c = create :course
s = create :submission, user: u, course: c
create :api_token, user: u
create :event, user: u
create :export, user: u
create :notification, user: u
create :annotation, user: u, submission: s
create :question, submission: s
end

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal 2, u2.submissions.count
assert_equal 2, u2.api_tokens.count
assert_equal 2, u2.events.count
assert_equal 2, u2.exports.count
assert_equal 4, u2.notifications.count
assert_equal 4, u2.annotations.count
assert_equal 2, u2.questions.count
end

test 'merge should only transfer unique read states to the other user' do
u1 = create :user
u2 = create :user

a1 = create :content_page
a2 = create :content_page
create :activity_read_state, user: u1, activity: a1
create :activity_read_state, user: u2, activity: a1
create :activity_read_state, user: u1, activity: a2

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal 2, u2.activity_read_states.count
end

test 'merge should only transfer unique identities to the other user' do
u1 = create :user
u2 = create :user

p1 = create :provider
p2 = create :provider
Identity.create user: u1, provider: p1, identifier: 'a'
Identity.create user: u2, provider: p1, identifier: 'b'
Identity.create user: u1, provider: p2, identifier: 'c'

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal 2, u2.identities.count
end

test 'merge should only transfer unique repositories to the other user' do
u1 = create :user
u2 = create :user

r1 = create :repository, :git_stubbed
r2 = create :repository, :git_stubbed
RepositoryAdmin.create user: u1, repository: r1
RepositoryAdmin.create user: u2, repository: r1
RepositoryAdmin.create user: u1, repository: r2

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal 2, u2.repository_admins.count
end

test 'merge should only transfer unique evaluations to the other user' do
u1 = create :user
u2 = create :user

e1 = create :evaluation
e2 = create :evaluation
EvaluationUser.create user: u1, evaluation: e1
EvaluationUser.create user: u2, evaluation: e1
EvaluationUser.create user: u1, evaluation: e2

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal 2, u2.evaluation_users.count
end

test 'merge should transfer course membership with most rights to the other user' do
u1 = create :user
u2 = create :user

c1 = create :course
c2 = create :course
c3 = create :course
c4 = create :course
c5 = create :course
CourseMembership.create user: u1, course: c1, status: 'student', favorite: true
CourseMembership.create user: u1, course: c2, status: 'pending'
CourseMembership.create user: u1, course: c3, status: 'unsubscribed'
CourseMembership.create user: u1, course: c4, status: 'student'
CourseMembership.create user: u1, course: c5, status: 'course_admin'

CourseMembership.create user: u2, course: c2, status: 'pending'
CourseMembership.create user: u2, course: c3, status: 'course_admin', favorite: true
CourseMembership.create user: u2, course: c4, status: 'unsubscribed'
CourseMembership.create user: u2, course: c5, status: 'student'

result = u1.merge_into(u2)

assert_equal 0, u1.course_memberships.count

assert result
assert_not u1.persisted?
assert_equal 5, u2.course_memberships.count
assert_equal 4, u2.subscribed_courses.count
assert_equal 2, u2.favorite_courses.count
assert_equal 2, u2.administrating_courses.count
assert_equal 2, u2.enrolled_courses.count
assert_equal 1, u2.pending_courses.count
assert_equal 0, u2.unsubscribed_courses.count
end

test 'merge should transfer update cached values' do
u1 = create :user
u2 = create :user

c = create :course
c2 = create :course
CourseMembership.create user: u1, course: c, status: 'student'
CourseMembership.create user: u2, course: c, status: 'student'
s1 = create :series, course: c, exercise_count: 0
s2 = create :series, course: c2, exercise_count: 0
e1 = create :exercise
e2 = create :exercise
e3 = create :exercise
SeriesMembership.create series: s1, activity: e1
SeriesMembership.create series: s1, activity: e2
SeriesMembership.create series: s2, activity: e3
create :correct_submission, user: u2, course: c, exercise: e1
create :wrong_submission, user: u2, course: c, exercise: e2
create :correct_submission, user: u1, course: c, exercise: e1
create :correct_submission, user: u1, course: c, exercise: e2
create :wrong_submission, user: u1, course: c2, exercise: e3

assert_equal 3, c.correct_solutions
assert_equal 2, c.subscribed_members_count
assert_equal 1, u2.correct_exercises
assert_equal 2, u2.attempted_exercises
assert_equal 2, e1.users_correct
assert_equal 2, e1.users_tried
assert_equal false, s1.completed?(user: u2)
assert_equal false, s2.started?(user: u2)
assert_equal false, s2.wrong?(user: u2)

result = u1.merge_into(u2)

assert result
assert_not u1.persisted?
assert_equal 1, c.subscribed_members_count
assert_equal 2, c.correct_solutions
assert_equal 2, u2.correct_exercises
assert_equal 3, u2.attempted_exercises
assert_equal 1, e1.users_correct
assert_equal 1, e1.users_tried
assert_equal true, s1.completed?(user: u2)
assert_equal true, s2.started?(user: u2)
assert_equal true, s2.wrong?(user: u2)
end
end
Loading