FEATURE: Add button to delete unused tags (#6587)
This is particularly useful if you have uploaded a CSV file, and wish to bulk-delete all of the tags that you uploaded.
This commit is contained in:
parent
182b34243d
commit
d89ffbeffd
|
@ -22,6 +22,12 @@ export default DropdownSelectBoxComponent.extend({
|
||||||
name: I18n.t("tagging.upload"),
|
name: I18n.t("tagging.upload"),
|
||||||
description: I18n.t("tagging.upload_description"),
|
description: I18n.t("tagging.upload_description"),
|
||||||
icon: "upload"
|
icon: "upload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "deleteUnusedTags",
|
||||||
|
name: I18n.t("tagging.delete_unused"),
|
||||||
|
description: I18n.t("tagging.delete_unused_description"),
|
||||||
|
icon: "trash"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -30,7 +36,8 @@ export default DropdownSelectBoxComponent.extend({
|
||||||
|
|
||||||
actionNames: {
|
actionNames: {
|
||||||
manageGroups: "showTagGroups",
|
manageGroups: "showTagGroups",
|
||||||
uploadTags: "showUploader"
|
uploadTags: "showUploader",
|
||||||
|
deleteUnusedTags: "deleteUnused"
|
||||||
},
|
},
|
||||||
|
|
||||||
mutateValue(id) {
|
mutateValue(id) {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import computed from "ember-addons/ember-computed-decorators";
|
import computed from "ember-addons/ember-computed-decorators";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
|
||||||
export default Ember.Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
sortProperties: ["totalCount:desc", "id"],
|
sortProperties: ["totalCount:desc", "id"],
|
||||||
|
@ -38,6 +40,41 @@ export default Ember.Controller.extend({
|
||||||
|
|
||||||
showUploader() {
|
showUploader() {
|
||||||
showModal("tag-upload");
|
showModal("tag-upload");
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUnused() {
|
||||||
|
ajax("/tags/unused", { type: "GET" })
|
||||||
|
.then(result => {
|
||||||
|
const displayN = 20;
|
||||||
|
const tags = result["tags"];
|
||||||
|
const tagString = tags.slice(0, displayN).join(", ");
|
||||||
|
var more = Math.max(0, tags.length - displayN);
|
||||||
|
const string =
|
||||||
|
more === 0
|
||||||
|
? I18n.t("tagging.delete_unused_confirmation", {
|
||||||
|
count: tags.length,
|
||||||
|
tags: tagString
|
||||||
|
})
|
||||||
|
: I18n.t("tagging.delete_unused_confirmation_more", {
|
||||||
|
total: tags.length,
|
||||||
|
tags: tagString,
|
||||||
|
count: more
|
||||||
|
});
|
||||||
|
|
||||||
|
bootbox.confirm(
|
||||||
|
string,
|
||||||
|
I18n.t("tagging.cancel_delete_unused"),
|
||||||
|
I18n.t("tagging.delete_unused"),
|
||||||
|
proceed => {
|
||||||
|
if (proceed) {
|
||||||
|
ajax("/tags/unused", { type: "DELETE" })
|
||||||
|
.then(() => this.send("refresh"))
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -146,6 +146,19 @@ class TagsController < ::ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def list_unused
|
||||||
|
guardian.ensure_can_admin_tags!
|
||||||
|
render json: { tags: Tag.unused.pluck(:name) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy_unused
|
||||||
|
guardian.ensure_can_admin_tags!
|
||||||
|
tags = Tag.unused
|
||||||
|
StaffActionLogger.new(current_user).log_custom('deleted_unused_tags', tags: tags.pluck(:name))
|
||||||
|
tags.destroy_all
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
guardian.ensure_can_admin_tags!
|
guardian.ensure_can_admin_tags!
|
||||||
tag_name = params[:tag_id]
|
tag_name = params[:tag_id]
|
||||||
|
|
|
@ -9,6 +9,8 @@ class Tag < ActiveRecord::Base
|
||||||
where("lower(name) IN (?)", name)
|
where("lower(name) IN (?)", name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
scope :unused, -> { where(topic_count: 0, pm_topic_count: 0) }
|
||||||
|
|
||||||
has_many :tag_users # notification settings
|
has_many :tag_users # notification settings
|
||||||
|
|
||||||
has_many :topic_tags, dependent: :destroy
|
has_many :topic_tags, dependent: :destroy
|
||||||
|
|
|
@ -2710,6 +2710,15 @@ en:
|
||||||
upload_description: "Upload a text file to create tags in bulk"
|
upload_description: "Upload a text file to create tags in bulk"
|
||||||
upload_instructions: "One per line, optionally with a tag group in the format 'tag_name,tag_group'."
|
upload_instructions: "One per line, optionally with a tag group in the format 'tag_name,tag_group'."
|
||||||
upload_successful: "Tags uploaded successfully"
|
upload_successful: "Tags uploaded successfully"
|
||||||
|
delete_unused_confirmation:
|
||||||
|
one: "1 tag will be deleted: %{tags}"
|
||||||
|
other: "{{count}} tags will be deleted: %{tags}"
|
||||||
|
delete_unused_confirmation_more:
|
||||||
|
one: "{{total}} tags will be deleted: %{tags} and one more"
|
||||||
|
other: "{{total}} tags will be deleted: %{tags} and %{count} more"
|
||||||
|
delete_unused: "Delete Unused Tags"
|
||||||
|
delete_unused_description: "Delete all tags which are not attached to any topics or personal messages"
|
||||||
|
cancel_delete_unused: "Cancel"
|
||||||
filters:
|
filters:
|
||||||
without_category: "%{filter} %{tag} topics"
|
without_category: "%{filter} %{tag} topics"
|
||||||
with_category: "%{filter} %{tag} topics in %{category}"
|
with_category: "%{filter} %{tag} topics in %{category}"
|
||||||
|
@ -3534,6 +3543,7 @@ en:
|
||||||
revoke_moderation: "revoke moderation"
|
revoke_moderation: "revoke moderation"
|
||||||
backup_create: "create backup"
|
backup_create: "create backup"
|
||||||
deleted_tag: "deleted tag"
|
deleted_tag: "deleted tag"
|
||||||
|
deleted_unused_tags: "deleted unused tags"
|
||||||
renamed_tag: "renamed tag"
|
renamed_tag: "renamed tag"
|
||||||
revoke_email: "revoke email"
|
revoke_email: "revoke email"
|
||||||
lock_trust_level: "lock trust level"
|
lock_trust_level: "lock trust level"
|
||||||
|
|
|
@ -776,6 +776,8 @@ Discourse::Application.routes.draw do
|
||||||
get '/check' => 'tags#check_hashtag'
|
get '/check' => 'tags#check_hashtag'
|
||||||
get '/personal_messages/:username' => 'tags#personal_messages'
|
get '/personal_messages/:username' => 'tags#personal_messages'
|
||||||
post '/upload' => 'tags#upload'
|
post '/upload' => 'tags#upload'
|
||||||
|
get '/unused' => 'tags#list_unused'
|
||||||
|
delete '/unused' => 'tags#destroy_unused'
|
||||||
constraints(tag_id: /[^\/]+?/, format: /json|rss/) do
|
constraints(tag_id: /[^\/]+?/, format: /json|rss/) do
|
||||||
get '/:tag_id.rss' => 'tags#tag_feed'
|
get '/:tag_id.rss' => 'tags#tag_feed'
|
||||||
get '/:tag_id' => 'tags#show', as: 'tag_show'
|
get '/:tag_id' => 'tags#show', as: 'tag_show'
|
||||||
|
|
|
@ -172,4 +172,18 @@ describe Tag do
|
||||||
expect(tag.topic_count).to eq(1)
|
expect(tag.topic_count).to eq(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "unused tags scope" do
|
||||||
|
let!(:tags) do
|
||||||
|
[ Fabricate(:tag, name: "used_publically", topic_count: 2, pm_topic_count: 0),
|
||||||
|
Fabricate(:tag, name: "used_privately", topic_count: 0, pm_topic_count: 3),
|
||||||
|
Fabricate(:tag, name: "used_everywhere", topic_count: 0, pm_topic_count: 3),
|
||||||
|
Fabricate(:tag, name: "unused1", topic_count: 0, pm_topic_count: 0),
|
||||||
|
Fabricate(:tag, name: "unused2", topic_count: 0, pm_topic_count: 0)]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the correct tags" do
|
||||||
|
expect(Tag.unused.pluck(:name)).to contain_exactly("unused1", "unused2")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -389,6 +389,45 @@ describe TagsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#unused' do
|
||||||
|
it "fails if you can't manage tags" do
|
||||||
|
sign_in(Fabricate(:user))
|
||||||
|
get "/tags/unused.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
delete "/tags/unused.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'logged in' do
|
||||||
|
before do
|
||||||
|
sign_in(Fabricate(:admin))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with some tags' do
|
||||||
|
let!(:tags) { [
|
||||||
|
Fabricate(:tag, name: "used_publically", topic_count: 2, pm_topic_count: 0),
|
||||||
|
Fabricate(:tag, name: "used_privately", topic_count: 0, pm_topic_count: 3),
|
||||||
|
Fabricate(:tag, name: "used_everywhere", topic_count: 0, pm_topic_count: 3),
|
||||||
|
Fabricate(:tag, name: "unused1", topic_count: 0, pm_topic_count: 0),
|
||||||
|
Fabricate(:tag, name: "unused2", topic_count: 0, pm_topic_count: 0)
|
||||||
|
]}
|
||||||
|
|
||||||
|
it 'returns the correct unused tags' do
|
||||||
|
get "/tags/unused.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
json = ::JSON.parse(response.body)
|
||||||
|
expect(json["tags"]).to contain_exactly("unused1", "unused2")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the correct tags' do
|
||||||
|
expect { delete "/tags/unused.json" }.to change { Tag.count }.by(-2) & change { UserHistory.count }.by(1)
|
||||||
|
expect(Tag.pluck(:name)).to contain_exactly("used_publically", "used_privately", "used_everywhere")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context '#upload_csv' do
|
context '#upload_csv' do
|
||||||
it 'requires you to be logged in' do
|
it 'requires you to be logged in' do
|
||||||
post "/tags/upload.json"
|
post "/tags/upload.json"
|
||||||
|
|
Loading…
Reference in New Issue