FEATURE: Upload tags from CSV (#6484)
This commit is contained in:
parent
8fa59f0548
commit
7ac08f936e
|
@ -0,0 +1,19 @@
|
|||
import UploadMixin from "discourse/mixins/upload";
|
||||
|
||||
export default Em.Component.extend(UploadMixin, {
|
||||
type: "csv",
|
||||
uploadUrl: "/tags/upload",
|
||||
addDisabled: Em.computed.alias("uploading"),
|
||||
elementId: "tag-uploader",
|
||||
|
||||
validateUploadedFilesOptions() {
|
||||
return { csvOnly: true };
|
||||
},
|
||||
|
||||
uploadDone() {
|
||||
bootbox.alert(I18n.t("tagging.upload_successful"), () => {
|
||||
this.sendAction("refresh");
|
||||
this.sendAction("closeModal");
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
<label class="btn {{if addDisabled 'disabled'}}">
|
||||
{{d-icon "upload"}}
|
||||
{{i18n 'admin.watched_words.form.upload'}}
|
||||
<input disabled={{addDisabled}} type="file" accept="text/plain,text/csv" style="visibility: hidden; position: absolute;" />
|
||||
</label>
|
||||
<span class="instructions">{{i18n 'tagging.upload_instructions'}}</span>
|
|
@ -16,6 +16,12 @@ export default DropdownSelectBoxComponent.extend({
|
|||
name: I18n.t("tagging.manage_groups"),
|
||||
description: I18n.t("tagging.manage_groups_description"),
|
||||
icon: "wrench"
|
||||
},
|
||||
{
|
||||
id: "uploadTags",
|
||||
name: I18n.t("tagging.upload"),
|
||||
description: I18n.t("tagging.upload_description"),
|
||||
icon: "upload"
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -23,7 +29,8 @@ export default DropdownSelectBoxComponent.extend({
|
|||
},
|
||||
|
||||
actionNames: {
|
||||
manageGroups: "showTagGroups"
|
||||
manageGroups: "showTagGroups",
|
||||
uploadTags: "showUploader"
|
||||
},
|
||||
|
||||
mutateValue(id) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
sortProperties: ["totalCount:desc", "id"],
|
||||
|
@ -33,6 +34,10 @@ export default Ember.Controller.extend({
|
|||
sortedByCount: false,
|
||||
sortedByName: true
|
||||
});
|
||||
},
|
||||
|
||||
showUploader() {
|
||||
showModal("tag-upload");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -41,6 +41,10 @@ export default Discourse.Route.extend({
|
|||
showTagGroups() {
|
||||
this.transitionTo("tagGroups");
|
||||
return true;
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{{#d-modal-body title='tagging.upload'}}
|
||||
{{tags-uploader closeModal=(action "closeModal") refresh=(route-action "refresh")}}
|
||||
{{/d-modal-body}}
|
|
@ -117,6 +117,35 @@ class TagsController < ::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def upload
|
||||
guardian.ensure_can_admin_tags!
|
||||
|
||||
file = params[:file] || params[:files].first
|
||||
|
||||
hijack do
|
||||
begin
|
||||
Tag.transaction do
|
||||
CSV.foreach(file.tempfile) do |row|
|
||||
raise Discourse::InvalidParameters.new(I18n.t("tags.upload_row_too_long")) if row.length > 2
|
||||
|
||||
tag_name = DiscourseTagging.clean_tag(row[0])
|
||||
tag_group_name = row[1] || nil
|
||||
|
||||
tag = Tag.find_by_name(tag_name) || Tag.create!(name: tag_name)
|
||||
|
||||
if tag_group_name
|
||||
tag_group = TagGroup.find_by(name: tag_group_name) || TagGroup.create!(name: tag_group_name)
|
||||
tag.tag_groups << tag_group unless tag.tag_groups.include?(tag_group)
|
||||
end
|
||||
end
|
||||
end
|
||||
render json: success_json
|
||||
rescue Discourse::InvalidParameters => e
|
||||
render json: failed_json.merge(errors: [e.message]), status: 422
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
guardian.ensure_can_admin_tags!
|
||||
tag_name = params[:tag_id]
|
||||
|
|
|
@ -2700,7 +2700,10 @@ en:
|
|||
sort_by_name: "name"
|
||||
manage_groups: "Manage Tag Groups"
|
||||
manage_groups_description: "Define groups to organize tags"
|
||||
|
||||
upload: "Upload Tags"
|
||||
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_successful: "Tags uploaded successfully"
|
||||
filters:
|
||||
without_category: "%{filter} %{tag} topics"
|
||||
with_category: "%{filter} %{tag} topics in %{category}"
|
||||
|
|
|
@ -3903,6 +3903,7 @@ en:
|
|||
staff_tag_disallowed: "The tag \"%{tag}\" may only be applied by staff."
|
||||
staff_tag_remove_disallowed: "The tag \"%{tag}\" may only be removed by staff."
|
||||
minimum_required_tags: "You must select at least %{count} tags."
|
||||
upload_row_too_long: "The CSV file should have one tag per line. Optionally the tag can be followed by a comma, then the tag group name."
|
||||
rss_by_tag: "Topics tagged %{tag}"
|
||||
|
||||
finish_installation:
|
||||
|
|
|
@ -775,6 +775,7 @@ Discourse::Application.routes.draw do
|
|||
get '/filter/search' => 'tags#search'
|
||||
get '/check' => 'tags#check_hashtag'
|
||||
get '/personal_messages/:username' => 'tags#personal_messages'
|
||||
post '/upload' => 'tags#upload'
|
||||
constraints(tag_id: /[^\/]+?/, format: /json|rss/) do
|
||||
get '/:tag_id.rss' => 'tags#tag_feed'
|
||||
get '/:tag_id' => 'tags#show', as: 'tag_show'
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
tag1
|
||||
Capitaltag2
|
||||
spaced tag
|
||||
tag1
|
||||
tag3,taggroup1
|
||||
tag4,taggroup1
|
|
|
@ -0,0 +1,5 @@
|
|||
tag1
|
||||
tag2
|
||||
tag3,taggroup1
|
||||
tag4,taggroup2
|
||||
tag5,with,too,many,columns
|
|
|
@ -369,4 +369,50 @@ describe TagsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context '#upload_csv' do
|
||||
it 'requires you to be logged in' do
|
||||
post "/tags/upload.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
context 'while logged in' do
|
||||
let(:csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/tags.csv") }
|
||||
let(:invalid_csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/tags_invalid.csv") }
|
||||
|
||||
let(:file) do
|
||||
Rack::Test::UploadedFile.new(File.open(csv_file))
|
||||
end
|
||||
|
||||
let(:invalid_file) do
|
||||
Rack::Test::UploadedFile.new(File.open(invalid_csv_file))
|
||||
end
|
||||
|
||||
let(:filename) { 'tags.csv' }
|
||||
|
||||
it "fails if you can't manage tags" do
|
||||
sign_in(Fabricate(:user))
|
||||
post "/tags/upload.json", params: { file: file, name: filename }
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "allows staff to bulk upload tags" do
|
||||
sign_in(Fabricate(:moderator))
|
||||
post "/tags/upload.json", params: { file: file, name: filename }
|
||||
expect(response.status).to eq(200)
|
||||
expect(Tag.pluck(:name)).to contain_exactly("tag1", "capitaltag2", "spaced-tag", "tag3", "tag4")
|
||||
expect(Tag.find_by_name("tag3").tag_groups.pluck(:name)).to contain_exactly("taggroup1")
|
||||
expect(Tag.find_by_name("tag4").tag_groups.pluck(:name)).to contain_exactly("taggroup1")
|
||||
end
|
||||
|
||||
it "fails gracefully with invalid input" do
|
||||
sign_in(Fabricate(:moderator))
|
||||
|
||||
expect do
|
||||
post "/tags/upload.json", params: { file: invalid_file, name: filename }
|
||||
expect(response.status).to eq(422)
|
||||
end.not_to change { [Tag.count, TagGroup.count] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue