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 accounts #1683

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7533775
merge simple attributes
fherreazcue Nov 20, 2023
92fc04a
destroy other person after merge
fherreazcue Nov 21, 2023
beb336d
merge group_memberships (work_groups and projects)
fherreazcue Nov 21, 2023
8a5f37c
Guard 'destroy' and back-up other person's details in tests before de…
fherreazcue Nov 21, 2023
c58e87b
merge annotations
fherreazcue Nov 21, 2023
7a90b11
Programmes and institutions also covered by group_memberships
fherreazcue Nov 23, 2023
525d8f1
check not merging empty arrays
fherreazcue Nov 23, 2023
c43cdae
merge subscriptions
fherreazcue Nov 24, 2023
a34e8c0
Merge resources (contributor and creator)
fherreazcue Nov 24, 2023
1daacd7
Merge permissions
fherreazcue Nov 24, 2023
f0b8dbf
Merge roles
fherreazcue Nov 27, 2023
4802091
Refactor merge_associations
fherreazcue Nov 28, 2023
5e1f0c9
Merge user
fherreazcue Nov 28, 2023
7a34d00
Adds activity log
fherreazcue Nov 28, 2023
9f7ce4c
rubocop
fherreazcue Nov 28, 2023
ec4c515
Person.transaction wraps all
fherreazcue Nov 28, 2023
003c0ce
Use send instead of constantize
fherreazcue Nov 28, 2023
ebb6463
Use in_batches for update_all or destroy_all.
fherreazcue Nov 29, 2023
40fcf76
merge_user also interrupted if no other_person.user
fherreazcue Nov 29, 2023
8d64fe9
Moved simple_attributes and annotation_types lists into merge methods.
fherreazcue Nov 29, 2023
8938b7a
Improved merge all types of resources test
fherreazcue Nov 29, 2023
35536fa
Merge annotations_by
fherreazcue Nov 29, 2023
f19cc32
simplify merge annotations test
fherreazcue Nov 30, 2023
122eb02
Improved merge permissions test
fherreazcue Nov 30, 2023
84926ca
Use destroy!
fherreazcue Nov 30, 2023
a436c70
Merge branch 'main' into merge-accounts
stuzart Feb 5, 2024
6e2b09b
Merge branch 'main' into merge-accounts
stuzart Feb 7, 2024
e7b8efc
Merge branch 'main' into merge-accounts
stuzart Jul 16, 2024
4cbc593
Merge branch 'main' into merge-accounts
stuzart Jul 18, 2024
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
2 changes: 2 additions & 0 deletions app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class Person < ApplicationRecord
after_destroy :updated_contributed_items_contributor_after_destroy
after_destroy :update_publication_authors_after_destroy

include Seek::Merging::PersonMerge

# to make it look like a User
def person
self
Expand Down
101 changes: 101 additions & 0 deletions lib/seek/merging/person_merge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module Seek
module Merging
module PersonMerge
def merge(other_person)
Person.transaction do
merge_simple_attributes(other_person)
merge_annotations(other_person)
# Roles have to be merged before group_memberships
merge_associations(other_person, 'roles', %i[scope_type scope_id role_type_id], { person_id: id })
# Merging group_memberships deals with work_groups, programmes, institutions and projects
merge_associations(other_person, 'group_memberships', [:work_group_id], { person_id: id })
merge_associations(other_person, 'subscriptions', [:subscribable_id], { person_id: id })
merge_associations(other_person, 'dependent_permissions', [:policy_id], { contributor_id: id })
merge_resources(other_person)
merge_user(other_person)

save!
other_person.reload # To prevent destruction of unlinked roles
other_person.destroy!
ActivityLog.create!(action: 'MERGE-person',
data: "Person with id #{other_person.id} was merged into person with id #{id}.")
end
end

private

def merge_simple_attributes(other_person)
# This attributes are directly copied from other_person if they are empty in the person to which its merging.
# first_letter is also updated
simple_attributes = %i[first_name last_name email phone skype_name web_page description avatar_id orcid]
simple_attributes.each do |attribute|
send("#{attribute}=", other_person.send(attribute)) if send(attribute).nil?
end
update_first_letter
end

def merge_annotations(other_person)
fherreazcue marked this conversation as resolved.
Show resolved Hide resolved
annotation_types = %w[expertise tools]
annotation_types.each do |annotation_type|
add_annotations(send(annotation_type) + other_person.send(annotation_type), annotation_type.singularize, self)
end

# Update annotations by other_user
return unless user && other_person.user

merge_user_associations(other_person, 'annotations_by',
%i[annotatable_type annotatable_id value_id],
{ source_id: user.id })
end

def merge_resources(other_person)
# Contributed
Person::RELATED_RESOURCE_TYPES.each do |resource_type|
other_person.send("contributed_#{resource_type.underscore.pluralize}").in_batches.update_all(contributor_id: id)
end
# Created
duplicated = other_person.created_items.pluck(:id) & created_items.pluck(:id)
AssetsCreator.where(creator_id: other_person.id, asset_id: duplicated).in_batches.destroy_all
AssetsCreator.where(creator_id: other_person.id).in_batches.update_all(creator_id: id)
end

def merge_associations(other_person, assoc, duplicates_match, update_hash)
other_items = other_person.send(assoc).pluck(*duplicates_match)
self_items = send(assoc).pluck(*duplicates_match)
duplicated = other_items & self_items
duplicated = duplicated.map { |item| [item] } if duplicates_match.length == 1

duplicated_hash = Hash[duplicates_match.zip(duplicated.transpose)]
other_person.send(assoc).where(duplicated_hash).in_batches.destroy_all

other_person.send(assoc).in_batches.update_all(update_hash)
end

def merge_user(other_person)
return unless user && other_person.user

merge_user_associations(other_person, 'identities',
%i[provider uid], { user_id: user.id })
merge_user_associations(other_person, 'oauth_applications',
%i[redirect_uri scopes], { owner_id: user.id })
merge_user_associations(other_person, 'access_tokens',
%i[application_id scopes], { resource_owner_id: user.id })
merge_user_associations(other_person, 'access_grants',
%i[application_id redirect_uri scopes], { resource_owner_id: user.id })
end
Comment on lines +74 to +85
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a search for belongs_to :user and found a few other associations:
image

The auth lookup tables will be too slow to update, but the various log tables are probably worth updating

Copy link
Member

@stuzart stuzart Nov 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the auth lookup table should update automatically. rows will be removed when the user is destroyed (another case where we can enhance the delete_all). After it's merged I also plan to check what jobs get created, or whether it just needs a job firing at the end


def merge_user_associations(other_person, assoc, duplicates_match, update_hash)
other_items = other_person.user.send(assoc).pluck(*duplicates_match)
self_items = user.send(assoc).pluck(*duplicates_match)
duplicated = other_items & self_items
duplicated = duplicated.map { |item| [item] } if duplicates_match.length == 1

duplicated_hash = Hash[duplicates_match.zip(duplicated.transpose)]
other_person.user.send(assoc).where(duplicated_hash).in_batches.destroy_all

other_person.user.send(assoc).in_batches.update_all(update_hash)
end

end
end
end
7 changes: 7 additions & 0 deletions test/factories/oauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@
expires_in { 3600 }
scopes { 'read' }
end

factory(:oauth_access_grant, class: Doorkeeper::AccessGrant) do
association :application, factory: :oauth_application
sequence(:redirect_uri) { |n| "https://localhost:3000/grant_#{n}" }
expires_in { 3600 }
scopes { 'read' }
end
end
Loading
Loading