FEATURE: wiki editors are allowed edit tags for wiki topics.

If a wiki editor's TL is greater than 'min trust level to tag topics' site setting then they can edit the tags for any wiki topic.
This commit is contained in:
Vinoth Kannan 2019-10-23 23:35:38 +05:30
parent 67ad8fbd1b
commit 31577b2131
12 changed files with 115 additions and 24 deletions

View File

@ -10,6 +10,7 @@ import afterTransition from "discourse/lib/after-transition";
export default Ember.Component.extend({
classNames: ["title-input"],
watchForLink: Ember.computed.alias("composer.canEditTopicFeaturedLink"),
disabled: Ember.computed.or("composer.loading", "composer.disableTitleInput"),
didInsertElement() {
this._super(...arguments);

View File

@ -186,6 +186,9 @@ export default Ember.Controller.extend({
);
},
disableCategoryChooser: Ember.computed.not("model.topic.details.can_edit"),
disableTagsChooser: Ember.computed.not("model.topic.canEditTags"),
isStaffUser: Ember.computed.reads("currentUser.staff"),
canUnlistTopic: Ember.computed.and("model.creatingTopic", "isStaffUser"),

View File

@ -141,6 +141,7 @@ const Composer = RestModel.extend({
creatingPrivateMessage: Ember.computed.equal("action", PRIVATE_MESSAGE),
notCreatingPrivateMessage: Ember.computed.not("creatingPrivateMessage"),
notPrivateMessage: Ember.computed.not("privateMessage"),
disableTitleInput: Ember.computed.not("topic.details.can_edit"),
@computed("privateMessage", "archetype.hasOptions")
showCategoryChooser(isPrivateMessage, hasOptions) {
@ -784,31 +785,31 @@ const Composer = RestModel.extend({
let promise = Ember.RSVP.resolve();
// Update the topic if we're editing the first post
if (
this.title &&
post.post_number === 1 &&
this.get("topic.details.can_edit")
) {
const topicProps = this.getProperties(
Object.keys(_edit_topic_serializer)
);
// frontend should have featuredLink but backend needs featured_link
if (topicProps.featuredLink) {
topicProps.featured_link = topicProps.featuredLink;
delete topicProps.featuredLink;
}
if (this.title && post.post_number === 1) {
const topic = this.topic;
// If we're editing a shared draft, keep the original category
if (this.action === EDIT_SHARED_DRAFT) {
const destinationCategoryId = topicProps.categoryId;
promise = promise.then(() =>
topic.updateDestinationCategory(destinationCategoryId)
if (topic.details.can_edit) {
const topicProps = this.getProperties(
Object.keys(_edit_topic_serializer)
);
topicProps.categoryId = topic.get("category.id");
// frontend should have featuredLink but backend needs featured_link
if (topicProps.featuredLink) {
topicProps.featured_link = topicProps.featuredLink;
delete topicProps.featuredLink;
}
// If we're editing a shared draft, keep the original category
if (this.action === EDIT_SHARED_DRAFT) {
const destinationCategoryId = topicProps.categoryId;
promise = promise.then(() =>
topic.updateDestinationCategory(destinationCategoryId)
);
topicProps.categoryId = topic.get("category.id");
}
promise = promise.then(() => Topic.update(topic, topicProps));
} else if (topic.details.can_edit_tags) {
promise = promise.then(() => topic.updateTags(this.tags));
}
promise = promise.then(() => Topic.update(topic, topicProps));
}
const props = {

View File

@ -546,6 +546,7 @@ const Topic = RestModel.extend({
readLastPost: propertyEqual("last_read_post_number", "highest_post_number"),
canClearPin: Ember.computed.and("pinned", "readLastPost"),
canEditTags: Ember.computed.or("details.can_edit", "details.can_edit_tags"),
archiveMessage() {
this.set("archiving", true);
@ -610,6 +611,17 @@ const Topic = RestModel.extend({
return ajax(`/t/${this.id}/reset-bump-date`, { type: "PUT" }).catch(
popupAjaxError
);
},
updateTags(tags) {
if (!tags || tags.length === 0) {
tags = [""];
}
return ajax(`/t/${this.id}/tags`, {
type: "PUT",
data: { tags: tags }
});
}
});

View File

@ -3,7 +3,7 @@
id="reply-title"
maxLength=titleMaxLength
placeholderKey=composer.titlePlaceholder
disabled=composer.loading
disabled=disabled
autocomplete="discourse"}}
{{popup-input-tip validation=validation}}

View File

@ -76,12 +76,13 @@
fullWidthOnMobile=true
value=model.categoryId
scopedCategoryId=scopedCategoryId
isDisabled=disableCategoryChooser
tabindex="3"}}
{{popup-input-tip validation=categoryValidation}}
</div>
{{/if}}
{{#if canEditTags}}
{{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags}}
{{mini-tag-chooser tags=model.tags tabindex="4" categoryId=model.categoryId minimum=model.minimumRequiredTags isDisabled=disableTagsChooser}}
{{popup-input-tip validation=tagValidation}}
{{/if}}
</div>

View File

@ -374,6 +374,16 @@ class TopicsController < ApplicationController
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
end
def update_tags
params.require(:tags)
topic = Topic.find_by(id: params[:topic_id])
guardian.ensure_can_edit_tags!(topic)
success = PostRevisor.new(topic.first_post, topic).revise!(current_user, { tags: params[:tags] }, validate_post: false)
success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
end
def feature_stats
params.require(:category_id)
category_id = params[:category_id].to_i

View File

@ -14,7 +14,8 @@ class TopicViewDetailsSerializer < ApplicationSerializer
:can_reply_as_new_topic,
:can_flag_topic,
:can_convert_topic,
:can_review_topic]
:can_review_topic,
:can_edit_tags]
end
attributes(
@ -128,6 +129,10 @@ class TopicViewDetailsSerializer < ApplicationSerializer
scope.can_convert_topic?(object.topic)
end
def include_can_edit_tags?
!scope.can_edit?(object.topic) && scope.can_edit_tags?(object.topic)
end
def allowed_users
object.topic.allowed_users.reject { |user| object.group_allowed_user_ids.include?(user.id) }
end

View File

@ -754,6 +754,7 @@ Discourse::Application.routes.draw do
delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: { topic_id: /\d+/ }
put "t/:topic_id/bookmark" => "topics#bookmark", constraints: { topic_id: /\d+/ }
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: { topic_id: /\d+/ }
put "t/:topic_id/tags" => "topics#update_tags", constraints: { topic_id: /\d+/ }
post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: { topic_id: /\d+/ }

View File

@ -184,4 +184,16 @@ module TopicGuardian
def can_banner_topic?(topic)
topic && authenticated? && !topic.private_message? && is_staff?
end
def can_edit_tags?(topic)
return false unless can_tag_topics?
return false if topic.private_message? && !can_tag_pms?
return true if can_edit_topic?(topic)
if topic&.first_post&.wiki && (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i)
return can_create_post?(topic)
end
false
end
end

View File

@ -1027,6 +1027,31 @@ RSpec.describe TopicsController do
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
end
it "can add a tag to wiki topic" do
SiteSetting.min_trust_to_edit_wiki_post = 2
topic.first_post.update!(wiki: true)
user = Fabricate(:user)
sign_in(user)
expect do
put "/t/#{topic.id}/tags.json", params: {
tags: [tag.name]
}
end.not_to change { topic.reload.first_post.revisions.count }
expect(response.status).to eq(403)
user.update!(trust_level: 2)
expect do
put "/t/#{topic.id}/tags.json", params: {
tags: [tag.name]
}
end.to change { topic.reload.first_post.revisions.count }.by(1)
expect(response.status).to eq(200)
expect(topic.tags.pluck(:id)).to contain_exactly(tag.id)
end
it 'does not remove tag if no params is given' do
topic.tags << tag

View File

@ -257,6 +257,26 @@ describe TopicViewSerializer do
expect(details[:allowed_users].find { |au| au[:id] == pm.user_id }).to be_present
expect(details[:allowed_groups].find { |ag| ag[:id] == group.id }).to be_present
end
context "can_edit_tags" do
before do
SiteSetting.tagging_enabled = true
SiteSetting.min_trust_to_edit_wiki_post = 2
end
it "returns true when user can edit a wiki topic" do
post = Fabricate(:post, wiki: true)
topic = Fabricate(:topic, first_post: post)
json = serialize_topic(topic, user)
expect(json[:details][:can_edit_tags]).to be_nil
user.update!(trust_level: 2)
json = serialize_topic(topic, user)
expect(json[:details][:can_edit_tags]).to eq(true)
end
end
end
end