mirror of
https://github.com/discourse/discourse.git
synced 2025-02-16 18:24:52 -06:00
FEATURE: admin UI to merge two users. (#9509)
This commit is contained in:
parent
13956017da
commit
a511bea4cc
@ -9,6 +9,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { fmt } from "discourse/lib/computed";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
export default Controller.extend(CanCheckEmails, {
|
||||
adminTools: service(),
|
||||
@ -207,6 +208,27 @@ export default Controller.extend(CanCheckEmails, {
|
||||
}
|
||||
},
|
||||
|
||||
promptTargetUser() {
|
||||
showModal("admin-merge-users-prompt", {
|
||||
admin: true,
|
||||
model: this.model
|
||||
});
|
||||
},
|
||||
|
||||
showMergeConfirmation(targetUsername) {
|
||||
showModal("admin-merge-users-confirmation", {
|
||||
admin: true,
|
||||
model: {
|
||||
username: this.model.username,
|
||||
targetUsername: targetUsername
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
merge(targetUsername) {
|
||||
return this.model.merge({ targetUsername: targetUsername });
|
||||
},
|
||||
|
||||
viewActionLogs() {
|
||||
this.adminTools.showActionLogs(this, {
|
||||
target_user: this.get("model.username")
|
||||
|
@ -0,0 +1,35 @@
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { alias } from "@ember/object/computed";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
adminUserIndex: controller(),
|
||||
username: alias("model.username"),
|
||||
targetUsername: alias("model.targetUsername"),
|
||||
|
||||
onShow() {
|
||||
this.set("value", null);
|
||||
},
|
||||
|
||||
@discourseComputed("username", "targetUsername")
|
||||
text(username, targetUsername) {
|
||||
return `transfer @${username} to @${targetUsername}`;
|
||||
},
|
||||
|
||||
@discourseComputed("value", "text")
|
||||
mergeDisabled(value, text) {
|
||||
return !value || text !== value;
|
||||
},
|
||||
|
||||
actions: {
|
||||
merge() {
|
||||
this.adminUserIndex.send("merge", this.targetUsername);
|
||||
this.send("closeModal");
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.send("closeModal");
|
||||
}
|
||||
}
|
||||
});
|
@ -0,0 +1,29 @@
|
||||
import Controller, { inject as controller } from "@ember/controller";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { alias } from "@ember/object/computed";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
adminUserIndex: controller(),
|
||||
username: alias("model.username"),
|
||||
|
||||
onShow() {
|
||||
this.set("targetUsername", null);
|
||||
},
|
||||
|
||||
@discourseComputed("username", "targetUsername")
|
||||
mergeDisabled(username, targetUsername) {
|
||||
return !targetUsername || username === targetUsername;
|
||||
},
|
||||
|
||||
actions: {
|
||||
merge() {
|
||||
this.send("closeModal");
|
||||
this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.send("closeModal");
|
||||
}
|
||||
}
|
||||
});
|
@ -503,6 +503,43 @@ const AdminUser = User.extend({
|
||||
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
|
||||
},
|
||||
|
||||
merge(opts) {
|
||||
const user = this;
|
||||
const location = document.location.pathname;
|
||||
|
||||
bootbox.dialog(I18n.t("admin.user.merging_user"));
|
||||
let formData = { context: location };
|
||||
|
||||
if (opts && opts.targetUsername) {
|
||||
formData["target_username"] = opts.targetUsername;
|
||||
}
|
||||
|
||||
return ajax(`/admin/users/${user.get("id")}/merge.json`, {
|
||||
type: "POST",
|
||||
data: formData
|
||||
})
|
||||
.then(function(data) {
|
||||
if (data.merged) {
|
||||
if (/^\/admin\/users\/list\//.test(location)) {
|
||||
document.location = location;
|
||||
} else {
|
||||
document.location = Discourse.getURL(
|
||||
`/admin/users/${data.user.id}/${data.user.username}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
bootbox.alert(I18n.t("admin.user.merge_failed"));
|
||||
if (data.user) {
|
||||
user.setProperties(data.user);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
AdminUser.find(user.get("id")).then(u => user.setProperties(u));
|
||||
bootbox.alert(I18n.t("admin.user.merge_failed"));
|
||||
});
|
||||
},
|
||||
|
||||
loadDetails() {
|
||||
if (this.loadedDetails) {
|
||||
return Promise.resolve(this);
|
||||
|
@ -0,0 +1,21 @@
|
||||
<div>
|
||||
{{#d-modal-body rawTitle=(i18n "admin.user.merge.confirmation.title" username=username)}}
|
||||
<p>{{html-safe (i18n "admin.user.merge.confirmation.description" username=username targetUsername=targetUsername text=text)}}</p>
|
||||
{{input type="text" value=value}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#d-button
|
||||
class="btn-danger"
|
||||
action=(action "merge")
|
||||
icon="trash-alt"
|
||||
disabled=mergeDisabled
|
||||
}}
|
||||
{{i18n "admin.user.merge.confirmation.transfer_and_delete" username=username}}
|
||||
{{/d-button}}
|
||||
{{d-button
|
||||
action=(action "cancel")
|
||||
label="admin.user.merge.confirmation.cancel"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
<div>
|
||||
{{#d-modal-body rawTitle=(i18n "admin.user.merge.prompt.title" username=username)}}
|
||||
<p>{{html-safe (i18n "admin.user.merge.prompt.description" username=username)}}</p>
|
||||
{{user-selector single=true
|
||||
placeholderKey="admin.user.merge.prompt.target_username_placeholder"
|
||||
usernames=targetUsername
|
||||
autocomplete="discourse"}}
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#d-button
|
||||
class="btn-primary"
|
||||
action=(action "merge")
|
||||
icon="trash-alt"
|
||||
disabled=mergeDisabled
|
||||
}}
|
||||
{{i18n "admin.user.merge.prompt.transfer_and_delete" username=username}}
|
||||
{{/d-button}}
|
||||
{{d-button
|
||||
action=(action "cancel")
|
||||
label="admin.user.merge.prompt.cancel"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
@ -680,9 +680,13 @@
|
||||
|
||||
{{#if model.canBeDeleted}}
|
||||
{{d-button label="admin.user.delete"
|
||||
icon="exclamation-triangle"
|
||||
icon="trash-alt"
|
||||
class="btn-danger"
|
||||
action=(action "destroy")}}
|
||||
{{d-button label="admin.user.merge.transfer_and_delete"
|
||||
icon="trash-alt"
|
||||
class="btn-danger"
|
||||
action=(action "promptTargetUser")}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
@ -20,6 +20,7 @@ class Admin::UsersController < Admin::AdminController
|
||||
:remove_group,
|
||||
:primary_group,
|
||||
:anonymize,
|
||||
:merge,
|
||||
:reset_bounce_score,
|
||||
:disable_second_factor,
|
||||
:delete_posts_batch]
|
||||
@ -471,6 +472,19 @@ class Admin::UsersController < Admin::AdminController
|
||||
end
|
||||
end
|
||||
|
||||
def merge
|
||||
target_username = params.require(:target_username)
|
||||
target_user = User.find_by_username(target_username)
|
||||
|
||||
guardian.ensure_can_merge_user!(@user, target_user)
|
||||
|
||||
if user = UserMerger.new(@user, target_user, current_user).merge!
|
||||
render json: success_json.merge(merged: true, user: user)
|
||||
else
|
||||
render json: failed_json.merge(user: AdminDetailedUserSerializer.new(@user, root: false).as_json)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_bounce_score
|
||||
guardian.ensure_can_reset_bounce_score!(@user)
|
||||
@user.user_stat&.reset_bounce_score!
|
||||
|
@ -24,6 +24,8 @@ class UserMerger
|
||||
delete_source_user
|
||||
delete_source_user_references
|
||||
log_merge
|
||||
|
||||
@target_user.reload
|
||||
end
|
||||
|
||||
protected
|
||||
|
@ -4334,6 +4334,32 @@ en:
|
||||
anonymize_yes: "Yes, anonymize this account"
|
||||
anonymize_failed: "There was a problem anonymizing the account."
|
||||
delete: "Delete User"
|
||||
merge:
|
||||
transfer_and_delete: "Transfer & Delete"
|
||||
prompt:
|
||||
title: "Transfer & Delete @%{username}"
|
||||
description: |
|
||||
<p>Please choose a new owner for <b>@%{username}'s</b> content.</p>
|
||||
|
||||
<p>All topics, posts, messages and other content created by <b>@%{username}</b> will be transferred.</p>
|
||||
|
||||
target_username_placeholder: "Username of new owner"
|
||||
transfer_and_delete: "Transfer & Delete @%{username}"
|
||||
cancel: "Cancel"
|
||||
confirmation:
|
||||
title: "Transfer & Delete @%{username}"
|
||||
description: |
|
||||
<p>All of <b>@%{username}'s</b> content will be transferred and attributed to <b>@%{targetUsername}</b>. After the content is transferred, <b>@%{username}'s</b> account will be deleted.</p>
|
||||
|
||||
<p><b>This can not be undone!</b></p>
|
||||
|
||||
<p>To continue type: <code>%{text}</code></p>
|
||||
|
||||
transfer_and_delete: "Transfer & Delete @%{username}"
|
||||
cancel: "Cancel"
|
||||
|
||||
merging_user: "Merging user..."
|
||||
merge_failed: "There was an error while merging the users."
|
||||
delete_forbidden_because_staff: "Admins and moderators can't be deleted."
|
||||
delete_posts_forbidden_because_staff: "Can't delete all posts of admins and moderators."
|
||||
delete_forbidden:
|
||||
|
@ -137,6 +137,7 @@ Discourse::Application.routes.draw do
|
||||
get "leader_requirements" => "users#tl3_requirements"
|
||||
get "tl3_requirements"
|
||||
put "anonymize"
|
||||
post "merge"
|
||||
post "reset_bounce_score"
|
||||
put "disable_second_factor"
|
||||
end
|
||||
|
@ -76,6 +76,10 @@ module UserGuardian
|
||||
is_staff? && !user.nil? && !user.staff?
|
||||
end
|
||||
|
||||
def can_merge_user?(source_user, target_user)
|
||||
is_admin? && !source_user.nil? && !source_user.staff? && !target_user.nil?
|
||||
end
|
||||
|
||||
def can_reset_bounce_score?(user)
|
||||
user && is_staff?
|
||||
end
|
||||
|
@ -988,4 +988,22 @@ RSpec.describe Admin::UsersController do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#merge" do
|
||||
fab!(:target_user) { Fabricate(:user) }
|
||||
fab!(:topic) { Fabricate(:topic, user: user) }
|
||||
fab!(:first_post) { Fabricate(:post, topic: topic, user: user) }
|
||||
|
||||
it 'should merge source user to target user' do
|
||||
post "/admin/users/#{user.id}/merge.json", params: {
|
||||
target_username: target_user.username
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["merged"]).to be_truthy
|
||||
expect(response.parsed_body["user"]["id"]).to eq(target_user.id)
|
||||
expect(topic.reload.user_id).to eq(target_user.id)
|
||||
expect(first_post.reload.user_id).to eq(target_user.id)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user