FEATURE: rake task for merging users
This commit is contained in:
parent
fffd1a6602
commit
7a2183e8ab
|
@ -0,0 +1,434 @@
|
|||
class UserMerger
|
||||
def initialize(source_user, target_user)
|
||||
@source_user = source_user
|
||||
@target_user = target_user
|
||||
end
|
||||
|
||||
def merge!
|
||||
update_notifications
|
||||
move_posts
|
||||
update_user_ids
|
||||
merge_given_daily_likes
|
||||
merge_post_timings
|
||||
merge_user_visits
|
||||
update_site_settings
|
||||
merge_user_attributes
|
||||
|
||||
DiscourseEvent.trigger(:merging_users, @source_user, @target_user)
|
||||
update_user_stats
|
||||
|
||||
delete_source_user
|
||||
delete_source_user_references
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def update_notifications
|
||||
params = {
|
||||
source_user_id: @source_user.id,
|
||||
source_username: @source_user.username,
|
||||
target_username: @target_user.username,
|
||||
notification_types_with_correct_user_id: [
|
||||
Notification.types[:granted_badge],
|
||||
Notification.types[:group_message_summary]
|
||||
],
|
||||
invitee_accepted_notification_type: Notification.types[:invitee_accepted]
|
||||
}
|
||||
|
||||
Notification.exec_sql(<<~SQL, params)
|
||||
UPDATE notifications AS n
|
||||
SET data = (data :: JSONB ||
|
||||
jsonb_strip_nulls(
|
||||
jsonb_build_object(
|
||||
'original_username', CASE data :: JSONB ->> 'original_username'
|
||||
WHEN :source_username
|
||||
THEN :target_username
|
||||
ELSE NULL END,
|
||||
'display_username', CASE data :: JSONB ->> 'display_username'
|
||||
WHEN :source_username
|
||||
THEN :target_username
|
||||
ELSE NULL END,
|
||||
'username', CASE data :: JSONB ->> 'username'
|
||||
WHEN :source_username
|
||||
THEN :target_username
|
||||
ELSE NULL END
|
||||
)
|
||||
)) :: JSON
|
||||
WHERE EXISTS(
|
||||
SELECT 1
|
||||
FROM posts AS p
|
||||
WHERE p.topic_id = n.topic_id
|
||||
AND p.post_number = n.post_number
|
||||
AND p.user_id = :source_user_id)
|
||||
OR (n.notification_type IN (:notification_types_with_correct_user_id) AND n.user_id = :source_user_id)
|
||||
OR (n.notification_type = :invitee_accepted_notification_type
|
||||
AND EXISTS(
|
||||
SELECT 1
|
||||
FROM invites i
|
||||
WHERE i.user_id = :source_user_id AND n.user_id = i.invited_by_id
|
||||
)
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
||||
def move_posts
|
||||
posts = Post.with_deleted
|
||||
.where(user_id: @source_user.id)
|
||||
.order(:topic_id, :post_number)
|
||||
.pluck(:topic_id, :id)
|
||||
|
||||
last_topic_id = nil
|
||||
post_ids = []
|
||||
|
||||
posts.each do |current_topic_id, current_post_id|
|
||||
if last_topic_id != current_topic_id && post_ids.any?
|
||||
change_post_owner(last_topic_id, post_ids)
|
||||
post_ids = []
|
||||
end
|
||||
|
||||
last_topic_id = current_topic_id
|
||||
post_ids << current_post_id
|
||||
end
|
||||
|
||||
change_post_owner(last_topic_id, post_ids) if post_ids.any?
|
||||
end
|
||||
|
||||
def change_post_owner(topic_id, post_ids)
|
||||
PostOwnerChanger.new(
|
||||
topic_id: topic_id,
|
||||
post_ids: post_ids,
|
||||
new_owner: @target_user,
|
||||
acting_user: Discourse.system_user,
|
||||
skip_revision: true
|
||||
).change_owner!
|
||||
end
|
||||
|
||||
def merge_given_daily_likes
|
||||
sql = <<~SQL
|
||||
INSERT INTO given_daily_likes AS g (user_id, likes_given, given_date, limit_reached)
|
||||
SELECT
|
||||
:target_user_id AS user_id,
|
||||
COUNT(1) AS likes_given,
|
||||
a.created_at::DATE AS given_date,
|
||||
COUNT(1) >= :max_likes_per_day AS limit_reached
|
||||
FROM post_actions AS a
|
||||
WHERE a.user_id = :target_user_id
|
||||
AND a.deleted_at IS NULL
|
||||
AND EXISTS(
|
||||
SELECT 1
|
||||
FROM given_daily_likes AS g
|
||||
WHERE g.user_id = :source_user_id AND a.created_at::DATE = g.given_date
|
||||
)
|
||||
GROUP BY given_date
|
||||
ON CONFLICT (user_id, given_date)
|
||||
DO UPDATE
|
||||
SET likes_given = EXCLUDED.likes_given,
|
||||
limit_reached = EXCLUDED.limit_reached
|
||||
SQL
|
||||
|
||||
GivenDailyLike.exec_sql(sql,
|
||||
source_user_id: @source_user.id,
|
||||
target_user_id: @target_user.id,
|
||||
max_likes_per_day: SiteSetting.max_likes_per_day,
|
||||
action_type_id: PostActionType.types[:like])
|
||||
end
|
||||
|
||||
def merge_post_timings
|
||||
update_user_id(:post_timings, conditions: ["x.topic_id = y.topic_id",
|
||||
"x.post_number = y.post_number"])
|
||||
sql = <<~SQL
|
||||
UPDATE post_timings AS t
|
||||
SET msecs = t.msecs + s.msecs
|
||||
FROM post_timings AS s
|
||||
WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
|
||||
AND t.topic_id = s.topic_id AND t.post_number = s.post_number
|
||||
SQL
|
||||
|
||||
PostTiming.exec_sql(sql, source_user_id: @source_user.id, target_user_id: @target_user.id)
|
||||
end
|
||||
|
||||
def merge_user_visits
|
||||
update_user_id(:user_visits, conditions: "x.visited_at = y.visited_at")
|
||||
|
||||
sql = <<~SQL
|
||||
UPDATE user_visits AS t
|
||||
SET posts_read = t.posts_read + s.posts_read,
|
||||
mobile = t.mobile OR s.mobile,
|
||||
time_read = t.time_read + s.time_read
|
||||
FROM user_visits AS s
|
||||
WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
|
||||
AND t.visited_at = s.visited_at
|
||||
SQL
|
||||
|
||||
UserVisit.exec_sql(sql, source_user_id: @source_user.id, target_user_id: @target_user.id)
|
||||
end
|
||||
|
||||
def update_site_settings
|
||||
SiteSetting.all_settings(true).each do |setting|
|
||||
if setting[:type] == "username" && setting[:value] == @source_user.username
|
||||
SiteSetting.set_and_log(setting[:setting], @target_user.username)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def update_user_stats
|
||||
# topics_entered
|
||||
UserStat.exec_sql(<<~SQL, target_user_id: @target_user.id)
|
||||
UPDATE user_stats
|
||||
SET topics_entered = (
|
||||
SELECT COUNT(topic_id)
|
||||
FROM topic_views
|
||||
WHERE user_id = :target_user_id
|
||||
)
|
||||
WHERE user_id = :target_user_id
|
||||
SQL
|
||||
|
||||
# time_read and days_visited
|
||||
UserStat.exec_sql(<<~SQL, target_user_id: @target_user.id)
|
||||
UPDATE user_stats
|
||||
SET time_read = COALESCE(x.time_read, 0),
|
||||
days_visited = COALESCE(x.days_visited, 0)
|
||||
FROM (
|
||||
SELECT
|
||||
SUM(time_read) AS time_read,
|
||||
COUNT(1) AS days_visited
|
||||
FROM user_visits
|
||||
WHERE user_id = :target_user_id
|
||||
) AS x
|
||||
WHERE user_id = :target_user_id
|
||||
SQL
|
||||
|
||||
# posts_read_count
|
||||
UserStat.exec_sql(<<~SQL, target_user_id: @target_user.id)
|
||||
UPDATE user_stats
|
||||
SET posts_read_count = (
|
||||
SELECT COUNT(1)
|
||||
FROM post_timings AS pt
|
||||
WHERE pt.user_id = :target_user_id AND EXISTS(
|
||||
SELECT 1
|
||||
FROM topics AS t
|
||||
WHERE t.archetype = 'regular' AND t.deleted_at IS NULL
|
||||
))
|
||||
WHERE user_id = :target_user_id
|
||||
SQL
|
||||
|
||||
# likes_given, likes_received, new_since, read_faq, first_post_created_at
|
||||
UserStat.exec_sql(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id)
|
||||
UPDATE user_stats AS t
|
||||
SET likes_given = t.likes_given + s.likes_given,
|
||||
likes_received = t.likes_received + s.likes_received,
|
||||
new_since = LEAST(t.new_since, s.new_since),
|
||||
read_faq = LEAST(t.read_faq, s.read_faq),
|
||||
first_post_created_at = LEAST(t.first_post_created_at, s.first_post_created_at)
|
||||
FROM user_stats AS s
|
||||
WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
|
||||
SQL
|
||||
end
|
||||
|
||||
def merge_user_attributes
|
||||
User.exec_sql(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id)
|
||||
UPDATE users AS t
|
||||
SET created_at = LEAST(t.created_at, s.created_at),
|
||||
updated_at = LEAST(t.updated_at, s.updated_at),
|
||||
seen_notification_id = GREATEST(t.seen_notification_id, s.seen_notification_id),
|
||||
last_posted_at = GREATEST(t.last_seen_at, s.last_seen_at),
|
||||
last_seen_at = GREATEST(t.last_seen_at, s.last_seen_at),
|
||||
admin = t.admin OR s.admin,
|
||||
last_emailed_at = GREATEST(t.last_emailed_at, s.last_emailed_at),
|
||||
trust_level = GREATEST(t.trust_level, s.trust_level),
|
||||
previous_visit_at = GREATEST(t.previous_visit_at, s.previous_visit_at),
|
||||
date_of_birth = COALESCE(t.date_of_birth, s.date_of_birth),
|
||||
ip_address = COALESCE(t.ip_address, s.ip_address),
|
||||
moderator = t.moderator OR s.moderator,
|
||||
title = COALESCE(t.title, s.title),
|
||||
primary_group_id = COALESCE(t.primary_group_id, s.primary_group_id),
|
||||
registration_ip_address = COALESCE(t.registration_ip_address, s.registration_ip_address),
|
||||
first_seen_at = LEAST(t.first_seen_at, s.first_seen_at),
|
||||
group_locked_trust_level = GREATEST(t.group_locked_trust_level, s.group_locked_trust_level),
|
||||
manual_locked_trust_level = GREATEST(t.manual_locked_trust_level, s.manual_locked_trust_level)
|
||||
FROM users AS s
|
||||
WHERE t.id = :target_user_id AND s.id = :source_user_id
|
||||
SQL
|
||||
|
||||
UserProfile.exec_sql(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id)
|
||||
UPDATE user_profiles AS t
|
||||
SET location = COALESCE(t.location, s.location),
|
||||
website = COALESCE(t.website, s.website),
|
||||
bio_raw = COALESCE(t.bio_raw, s.bio_raw),
|
||||
bio_cooked = COALESCE(t.bio_cooked, s.bio_cooked),
|
||||
bio_cooked_version = COALESCE(t.bio_cooked_version, s.bio_cooked_version),
|
||||
profile_background = COALESCE(t.profile_background, s.profile_background),
|
||||
dismissed_banner_key = COALESCE(t.dismissed_banner_key, s.dismissed_banner_key),
|
||||
badge_granted_title = t.badge_granted_title OR s.badge_granted_title,
|
||||
card_background = COALESCE(t.card_background, s.card_background),
|
||||
card_image_badge_id = COALESCE(t.card_image_badge_id, s.card_image_badge_id),
|
||||
views = t.views + s.views
|
||||
FROM user_profiles AS s
|
||||
WHERE t.user_id = :target_user_id AND s.user_id = :source_user_id
|
||||
SQL
|
||||
end
|
||||
|
||||
def update_user_ids
|
||||
Category.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
update_user_id(:category_users, conditions: ["x.category_id = y.category_id"])
|
||||
|
||||
update_user_id(:developers)
|
||||
|
||||
update_user_id(:draft_sequences, conditions: "x.draft_key = y.draft_key")
|
||||
update_user_id(:drafts, conditions: "x.draft_key = y.draft_key")
|
||||
|
||||
EmailLog.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
GroupHistory.where(acting_user_id: @source_user.id).update_all(acting_user_id: @target_user.id)
|
||||
GroupHistory.where(target_user_id: @source_user.id).update_all(target_user_id: @target_user.id)
|
||||
|
||||
update_user_id(:group_users, conditions: "x.group_id = y.group_id")
|
||||
|
||||
IncomingEmail.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
IncomingLink.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
IncomingLink.where(current_user_id: @source_user.id).update_all(current_user_id: @target_user.id)
|
||||
|
||||
Invite.with_deleted.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
Invite.with_deleted.where(invited_by_id: @source_user.id).update_all(invited_by_id: @target_user.id)
|
||||
Invite.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
|
||||
|
||||
update_user_id(:muted_users, conditions: "x.muted_user_id = y.muted_user_id")
|
||||
update_user_id(:muted_users, user_id_column_name: "muted_user_id", conditions: "x.user_id = y.user_id")
|
||||
|
||||
Notification.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
update_user_id(:post_actions, conditions: ["x.post_id = y.post_id",
|
||||
"x.post_action_type_id = y.post_action_type_id",
|
||||
"x.targets_topic = y.targets_topic"])
|
||||
|
||||
PostAction.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
|
||||
PostAction.where(deferred_by_id: @source_user.id).update_all(deferred_by_id: @target_user.id)
|
||||
PostAction.where(agreed_by_id: @source_user.id).update_all(agreed_by_id: @target_user.id)
|
||||
PostAction.where(disagreed_by_id: @source_user.id).update_all(disagreed_by_id: @target_user.id)
|
||||
|
||||
PostRevision.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
Post.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
|
||||
Post.with_deleted.where(last_editor_id: @source_user.id).update_all(last_editor_id: @target_user.id)
|
||||
Post.with_deleted.where(locked_by_id: @source_user.id).update_all(locked_by_id: @target_user.id)
|
||||
Post.with_deleted.where(reply_to_user_id: @source_user.id).update_all(reply_to_user_id: @target_user.id)
|
||||
|
||||
QueuedPost.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
QueuedPost.where(approved_by_id: @source_user.id).update_all(approved_by_id: @target_user.id)
|
||||
QueuedPost.where(rejected_by_id: @source_user.id).update_all(rejected_by_id: @target_user.id)
|
||||
|
||||
SearchLog.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
update_user_id(:tag_users, conditions: "x.tag_id = y.tag_id")
|
||||
|
||||
Theme.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
update_user_id(:topic_allowed_users, conditions: "x.topic_id = y.topic_id")
|
||||
|
||||
TopicEmbed.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
|
||||
|
||||
TopicLink.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
TopicLinkClick.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
TopicTimer.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
|
||||
|
||||
update_user_id(:topic_timers, conditions: ["x.status_type = y.status_type",
|
||||
"x.topic_id = y.topic_id",
|
||||
"y.deleted_at IS NULL"])
|
||||
|
||||
update_user_id(:topic_users, conditions: "x.topic_id = y.topic_id")
|
||||
|
||||
update_user_id(:topic_views, conditions: "x.topic_id = y.topic_id")
|
||||
|
||||
Topic.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id)
|
||||
|
||||
UnsubscribeKey.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
Upload.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
update_user_id(:user_archived_messages, conditions: "x.topic_id = y.topic_id")
|
||||
|
||||
update_user_id(:user_actions,
|
||||
user_id_column_name: "user_id",
|
||||
conditions: ["x.action_type = y.action_type",
|
||||
"x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id",
|
||||
"x.target_post_id IS NOT DISTINCT FROM y.target_post_id",
|
||||
"x.acting_user_id IN (:source_user_id, :target_user_id)"])
|
||||
update_user_id(:user_actions,
|
||||
user_id_column_name: "acting_user_id",
|
||||
conditions: ["x.action_type = y.action_type",
|
||||
"x.user_id = y.user_id",
|
||||
"x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id",
|
||||
"x.target_post_id IS NOT DISTINCT FROM y.target_post_id"])
|
||||
|
||||
update_user_id(:user_badges, conditions: ["x.badge_id = y.badge_id",
|
||||
"x.seq = y.seq",
|
||||
"x.post_id IS NOT DISTINCT FROM y.post_id"])
|
||||
|
||||
UserBadge.where(granted_by_id: @source_user.id).update_all(granted_by_id: @target_user.id)
|
||||
|
||||
update_user_id(:user_custom_fields, conditions: "x.name = y.name")
|
||||
|
||||
update_user_id(:user_emails, conditions: "x.email = y.email", updates: '"primary" = false')
|
||||
|
||||
UserExport.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
UserHistory.where(target_user_id: @source_user.id).update_all(target_user_id: @target_user.id)
|
||||
UserHistory.where(acting_user_id: @source_user.id).update_all(acting_user_id: @target_user.id)
|
||||
|
||||
UserProfileView.where(user_profile_id: @source_user.id).update_all(user_profile_id: @target_user.id)
|
||||
UserProfileView.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
|
||||
UserWarning.where(user_id: @source_user.id).update_all(user_id: @target_user.id)
|
||||
UserWarning.where(created_by_id: @source_user.id).update_all(created_by_id: @target_user.id)
|
||||
|
||||
User.where(approved_by_id: @source_user.id).update_all(approved_by_id: @target_user.id)
|
||||
end
|
||||
|
||||
def delete_source_user
|
||||
@source_user.reload
|
||||
@source_user.update_attribute(:admin, false)
|
||||
UserDestroyer.new(Discourse.system_user).destroy(@source_user)
|
||||
end
|
||||
|
||||
def delete_source_user_references
|
||||
Developer.where(user_id: @source_user.id).delete_all
|
||||
DraftSequence.where(user_id: @source_user.id).delete_all
|
||||
GivenDailyLike.where(user_id: @source_user.id).delete_all
|
||||
MutedUser.where(user_id: @source_user.id).or(MutedUser.where(muted_user_id: @source_user.id)).delete_all
|
||||
UserAuthTokenLog.where(user_id: @source_user.id).delete_all
|
||||
UserAvatar.where(user_id: @source_user.id).delete_all
|
||||
UserAction.where(acting_user_id: @source_user.id).delete_all
|
||||
end
|
||||
|
||||
def update_user_id(table_name, opts = {})
|
||||
builder = update_user_id_sql_builder(table_name, opts)
|
||||
builder.exec(source_user_id: @source_user.id, target_user_id: @target_user.id)
|
||||
end
|
||||
|
||||
def update_user_id_sql_builder(table_name, opts = {})
|
||||
user_id_column_name = opts[:user_id_column_name] || :user_id
|
||||
conditions = Array.wrap(opts[:conditions])
|
||||
updates = Array.wrap(opts[:updates])
|
||||
|
||||
builder = SqlBuilder.new(<<~SQL)
|
||||
UPDATE #{table_name} AS x
|
||||
/*set*/
|
||||
WHERE x.#{user_id_column_name} = :source_user_id AND NOT EXISTS(
|
||||
SELECT 1
|
||||
FROM #{table_name} AS y
|
||||
/*where*/
|
||||
)
|
||||
SQL
|
||||
|
||||
builder.set("#{user_id_column_name} = :target_user_id")
|
||||
updates.each { |u| builder.set(u) }
|
||||
|
||||
builder.where("y.#{user_id_column_name} = :target_user_id")
|
||||
conditions.each { |c| builder.where(c) }
|
||||
|
||||
builder
|
||||
end
|
||||
end
|
|
@ -4,20 +4,14 @@ task "users:change_post_ownership", [:old_username, :new_username, :archetype] =
|
|||
new_username = args[:new_username]
|
||||
archetype = args[:archetype]
|
||||
archetype = archetype.downcase if archetype
|
||||
|
||||
if !old_username || !new_username
|
||||
puts "ERROR: Expecting rake posts:change_post_ownership[old_username,new_username,archetype]"
|
||||
exit 1
|
||||
end
|
||||
old_user = User.find_by(username_lower: old_username.downcase)
|
||||
if !old_user
|
||||
puts "ERROR: User with username #{old_username} does not exist"
|
||||
exit 1
|
||||
end
|
||||
new_user = User.find_by(username_lower: new_username.downcase)
|
||||
if !new_user
|
||||
puts "ERROR: User with username #{new_username} does not exist"
|
||||
exit 1
|
||||
end
|
||||
|
||||
old_user = find_user(old_username)
|
||||
new_user = find_user(new_username)
|
||||
|
||||
if archetype == "private"
|
||||
posts = Post.private_posts.where(user_id: old_user.id)
|
||||
|
@ -37,3 +31,30 @@ task "users:change_post_ownership", [:old_username, :new_username, :archetype] =
|
|||
end
|
||||
puts "", "#{i} posts ownership changed!", ""
|
||||
end
|
||||
|
||||
task "users:merge", [:source_username, :target_username] => [:environment] do |_, args|
|
||||
source_username = args[:source_username]
|
||||
target_username = args[:target_username]
|
||||
|
||||
if !source_username || !target_username
|
||||
puts "ERROR: Expecting rake posts:merge[source_username,target_username]"
|
||||
exit 1
|
||||
end
|
||||
|
||||
source_user = find_user(source_username)
|
||||
target_user = find_user(target_username)
|
||||
|
||||
UserMerger.new(source_user, target_user).merge!
|
||||
puts "", "Users merged!", ""
|
||||
end
|
||||
|
||||
def find_user(username)
|
||||
user = User.find_by_username(username)
|
||||
|
||||
if !user
|
||||
puts "ERROR: User with username #{username} does not exist"
|
||||
exit 1
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
module DiscoursePoll
|
||||
class VotesUpdater
|
||||
def self.merge_users!(source_user, target_user)
|
||||
post_ids = PostCustomField.where(name: DiscoursePoll::VOTES_CUSTOM_FIELD)
|
||||
.where("value :: JSON -> ? IS NOT NULL", source_user.id.to_s)
|
||||
.pluck(:post_id)
|
||||
|
||||
post_ids.each do |post_id|
|
||||
DistributedMutex.synchronize("#{DiscoursePoll::MUTEX_PREFIX}-#{post_id}") do
|
||||
post = Post.find_by(id: post_id)
|
||||
update_votes(post, source_user, target_user) if post
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.update_votes(post, source_user, target_user)
|
||||
polls = post.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD]
|
||||
votes = post.custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD]
|
||||
return if polls.nil? || votes.nil? || !votes.has_key?(source_user.id.to_s)
|
||||
|
||||
if votes.has_key?(target_user.id.to_s)
|
||||
remove_votes(polls, votes, source_user)
|
||||
else
|
||||
replace_voter_id(polls, votes, source_user, target_user)
|
||||
end
|
||||
|
||||
post.save_custom_fields(true)
|
||||
end
|
||||
|
||||
def self.remove_votes(polls, votes, source_user)
|
||||
votes.delete(source_user.id.to_s).each do |poll_name, option_ids|
|
||||
poll = polls[poll_name]
|
||||
next unless poll && option_ids
|
||||
|
||||
poll["options"].each do |option|
|
||||
if option_ids.include?(option["id"])
|
||||
option["votes"] -= 1
|
||||
|
||||
voter_ids = option["voter_ids"]
|
||||
voter_ids.delete(source_user.id) if voter_ids
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.replace_voter_id(polls, votes, source_user, target_user)
|
||||
votes[target_user.id.to_s] = votes.delete(source_user.id.to_s)
|
||||
|
||||
polls.each_value do |poll|
|
||||
next unless poll["public"] == "true"
|
||||
|
||||
poll["options"].each do |option|
|
||||
voter_ids = option["voter_ids"]
|
||||
voter_ids << target_user.id if voter_ids&.delete(source_user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,10 +18,12 @@ after_initialize do
|
|||
DEFAULT_POLL_NAME ||= "poll".freeze
|
||||
POLLS_CUSTOM_FIELD ||= "polls".freeze
|
||||
VOTES_CUSTOM_FIELD ||= "polls-votes".freeze
|
||||
MUTEX_PREFIX ||= PLUGIN_NAME
|
||||
|
||||
autoload :PostValidator, "#{Rails.root}/plugins/poll/lib/post_validator"
|
||||
autoload :PollsValidator, "#{Rails.root}/plugins/poll/lib/polls_validator"
|
||||
autoload :PollsUpdater, "#{Rails.root}/plugins/poll/lib/polls_updater"
|
||||
autoload :VotesUpdater, "#{Rails.root}/plugins/poll/lib/votes_updater"
|
||||
|
||||
class Engine < ::Rails::Engine
|
||||
engine_name PLUGIN_NAME
|
||||
|
@ -385,6 +387,10 @@ after_initialize do
|
|||
polls: post.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD])
|
||||
end
|
||||
|
||||
on(:merging_users) do |source_user, target_user|
|
||||
DiscoursePoll::VotesUpdater.merge_users!(source_user, target_user)
|
||||
end
|
||||
|
||||
add_to_serializer(:post, :polls, false) do
|
||||
polls = post_custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD].dup
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe DiscoursePoll::VotesUpdater do
|
||||
let(:target_user) { Fabricate(:user_single_email, username: 'alice', email: 'alice@example.com') }
|
||||
let(:source_user) { Fabricate(:user_single_email, username: 'alice1', email: 'alice@work.com') }
|
||||
let(:walter) { Fabricate(:walter_white) }
|
||||
|
||||
let(:target_user_id) { target_user.id.to_s }
|
||||
let(:source_user_id) { source_user.id.to_s }
|
||||
let(:walter_id) { walter.id.to_s }
|
||||
|
||||
let(:post_with_two_polls) do
|
||||
raw = <<~RAW
|
||||
[poll type=multiple min=2 max=3 public=true]
|
||||
- Option 1
|
||||
- Option 2
|
||||
- Option 3
|
||||
[/poll]
|
||||
|
||||
[poll name=private_poll]
|
||||
- Option 1
|
||||
- Option 2
|
||||
- Option 3
|
||||
[/poll]
|
||||
RAW
|
||||
|
||||
Fabricate(:post, raw: raw)
|
||||
end
|
||||
|
||||
let(:option1_id) { "63eb791ab5d08fc4cc855a0703ac0dd1" }
|
||||
let(:option2_id) { "773a193533027393806fff6edd6c04f7" }
|
||||
let(:option3_id) { "f42f567ca3136ee1322d71d7745084c7" }
|
||||
|
||||
def vote(post, user, option_ids, poll_name = nil)
|
||||
poll_name ||= DiscoursePoll::DEFAULT_POLL_NAME
|
||||
DiscoursePoll::Poll.vote(post.id, poll_name, option_ids, user)
|
||||
end
|
||||
|
||||
it "should move votes to the target_user when only the source_user voted" do
|
||||
vote(post_with_two_polls, source_user, [option1_id, option3_id])
|
||||
vote(post_with_two_polls, walter, [option1_id, option2_id])
|
||||
|
||||
DiscoursePoll::VotesUpdater.merge_users!(source_user, target_user)
|
||||
post_with_two_polls.reload
|
||||
|
||||
polls = post_with_two_polls.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD]
|
||||
expect(polls["poll"]["options"][0]["votes"]).to eq(2)
|
||||
expect(polls["poll"]["options"][1]["votes"]).to eq(1)
|
||||
expect(polls["poll"]["options"][2]["votes"]).to eq(1)
|
||||
|
||||
expect(polls["poll"]["options"][0]["voter_ids"]).to contain_exactly(target_user.id, walter.id)
|
||||
expect(polls["poll"]["options"][1]["voter_ids"]).to contain_exactly(walter.id)
|
||||
expect(polls["poll"]["options"][2]["voter_ids"]).to contain_exactly(target_user.id)
|
||||
|
||||
votes = post_with_two_polls.custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD]
|
||||
expect(votes.keys).to contain_exactly(target_user_id, walter_id)
|
||||
expect(votes[target_user_id]["poll"]).to contain_exactly(option1_id, option3_id)
|
||||
expect(votes[walter_id]["poll"]).to contain_exactly(option1_id, option2_id)
|
||||
end
|
||||
|
||||
it "should delete votes of the source_user if the target_user voted" do
|
||||
vote(post_with_two_polls, source_user, [option1_id, option3_id])
|
||||
vote(post_with_two_polls, target_user, [option2_id, option3_id])
|
||||
vote(post_with_two_polls, walter, [option1_id, option2_id])
|
||||
|
||||
DiscoursePoll::VotesUpdater.merge_users!(source_user, target_user)
|
||||
post_with_two_polls.reload
|
||||
|
||||
polls = post_with_two_polls.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD]
|
||||
expect(polls["poll"]["options"][0]["votes"]).to eq(1)
|
||||
expect(polls["poll"]["options"][1]["votes"]).to eq(2)
|
||||
expect(polls["poll"]["options"][2]["votes"]).to eq(1)
|
||||
|
||||
expect(polls["poll"]["options"][0]["voter_ids"]).to contain_exactly(walter.id)
|
||||
expect(polls["poll"]["options"][1]["voter_ids"]).to contain_exactly(target_user.id, walter.id)
|
||||
expect(polls["poll"]["options"][2]["voter_ids"]).to contain_exactly(target_user.id)
|
||||
|
||||
votes = post_with_two_polls.custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD]
|
||||
expect(votes.keys).to contain_exactly(target_user_id, walter_id)
|
||||
expect(votes[target_user_id]["poll"]).to contain_exactly(option2_id, option3_id)
|
||||
expect(votes[walter_id]["poll"]).to contain_exactly(option1_id, option2_id)
|
||||
end
|
||||
|
||||
it "does not add voter_ids unless the poll is public" do
|
||||
vote(post_with_two_polls, source_user, [option1_id, option3_id], "private_poll")
|
||||
vote(post_with_two_polls, walter, [option1_id, option2_id], "private_poll")
|
||||
|
||||
DiscoursePoll::VotesUpdater.merge_users!(source_user, target_user)
|
||||
post_with_two_polls.reload
|
||||
|
||||
polls = post_with_two_polls.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD]
|
||||
polls["private_poll"]["options"].each { |o| expect(o).to_not have_key("voter_ids") }
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue