FEATURE: Allow staffs to tag PMs

This commit is contained in:
Vinoth Kannan 2018-02-14 02:16:25 +05:30
parent d525a644d2
commit 84ce1acfef
30 changed files with 163 additions and 141 deletions

View File

@ -1,2 +1,7 @@
// Injections don't occur without a class
export default Ember.Component.extend();
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
@computed('topic.isPrivateMessage')
});

View File

@ -140,7 +140,8 @@ export default Ember.Controller.extend({
return !this.site.mobileView &&
this.site.get('can_tag_topics') &&
canEditTitle &&
!creatingPrivateMessage;
!creatingPrivateMessage &&
(!this.get('model.topic.isPrivateMessage') || this.site.get('can_tag_pms'));
},
@computed('model.whisper', 'model.unlistTopic')

View File

@ -104,7 +104,7 @@ export default Ember.Controller.extend(BufferedContent, {
@computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics');
return this.site.get('can_tag_topics') && (!isPrivateMessage || this.site.get('can_tag_pms'));
},
actions: {

View File

@ -3,7 +3,12 @@ export default function renderTag(tag, params) {
tag = Handlebars.Utils.escapeExpression(tag);
const classes = ['tag-' + tag, 'discourse-tag'];
const tagName = params.tagName || "a";
const href = (tagName === "a" && !params.noHref) ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
let path;
if (tagName === "a" && !params.noHref) {
const current_user = Discourse.User.current();
path = params.isPrivateMessage ? `/u/${current_user.username}/messages/tag/${tag}` : `/tags/${tag}`;
}
const href = path ? ` href='${Discourse.getURL(path)}' ` : "";
if (Discourse.SiteSettings.tag_style || params.style) {
classes.push(params.style || Discourse.SiteSettings.tag_style);

View File

@ -20,6 +20,7 @@ export function addTagsHtmlCallback(callback, options) {
export default function(topic, params){
let tags = topic.tags;
let buffer = "";
const isPrivateMessage = topic.get('isPrivateMessage');
if (params && params.mode === "list") {
tags = topic.get("visibleListTags");
@ -43,7 +44,7 @@ export default function(topic, params){
buffer = "<div class='discourse-tags'>";
if (tags) {
for(let i=0; i<tags.length; i++){
buffer += renderTag(tags[i]) + ' ';
buffer += renderTag(tags[i], { isPrivateMessage }) + ' ';
}
}

View File

@ -96,6 +96,7 @@ export default function() {
this.route('archive');
this.route('group', { path: 'group/:name'});
this.route('groupArchive', { path: 'group/:name/archive'});
this.route('tag', { path: 'tag/:id'});
});
this.route('preferences', { resetNamespace: true }, function() {

View File

@ -0,0 +1,10 @@
import createPMRoute from "discourse/routes/build-private-messages-route";
export default createPMRoute('tags', 'private-messages-tags').extend({
model(params) {
const username = this.modelFor("user").get("username_lower");
return this.store.findFiltered("topicList", {
filter: `topics/private-messages-tag/${username}/${params.id}`
});
}
});

View File

@ -1,13 +1,13 @@
{{#if topic.category.parentCategory}}
{{bound-category-link topic.category.parentCategory}}
{{/if}}
{{bound-category-link topic.category hideParent=true}}
{{#unless topic.isPrivateMessage}}
{{#if topic.category.parentCategory}}
{{bound-category-link topic.category.parentCategory}}
{{/if}}
{{bound-category-link topic.category hideParent=true}}
{{/unless}}
<div class="topic-header-extra">
{{#if siteSettings.tagging_enabled}}
<div class="list-tags">
{{#each topic.tags as |t|}}
{{discourse-tag t}}
{{/each}}
{{discourse-tags topic mode="list"}}
</div>
{{/if}}
{{#if siteSettings.topic_featured_link_enabled}}

View File

@ -63,9 +63,7 @@
{{/if}}
</h1>
{{#unless model.isPrivateMessage}}
{{topic-category topic=model class="topic-category"}}
{{/unless}}
{{topic-category topic=model class="topic-category"}}
{{/if}}
{{/topic-title}}
{{/if}}

View File

@ -189,7 +189,7 @@
.category-input {
display: flex;
flex: 1 0 35%;
margin: 0 0 5px 10px;
margin: 0 5px 5px 10px;
@media screen and (max-width: 955px) {
flex: 1 0 100%;
margin-left: 0;
@ -223,7 +223,7 @@
.mini-tag-chooser {
flex: 1 1 25%;
margin: 0 0 5px 5px;
margin: 0 0 5px 0;
background: $secondary;
@media all and (max-width: 900px) {
margin: 0;

View File

@ -122,6 +122,10 @@ a.badge-category {
}
}
.archetype-private_message #topic-title .edit-topic-title .tag-chooser {
margin-left: 19px;
}
.private_message {
#topic-title {
.edit-topic-title {

View File

@ -151,6 +151,7 @@ class ListController < ApplicationController
private_messages_archive
private_messages_group
private_messages_group_archive
private_messages_tag
}.each do |action|
generate_message_route(action)
end
@ -332,6 +333,7 @@ class ListController < ApplicationController
def build_topic_list_options
options = {}
params[:page] = params[:page].to_i rescue 1
params[:tags] = [params[:tag_id]] if params[:tag_id].present? && guardian.can_tag_pms?
TopicQuery.public_valid_options.each do |key|
options[key] = params[key]

View File

@ -16,21 +16,6 @@ class Tag < ActiveRecord::Base
after_save :index_search
COUNT_ARG = "topics.id"
# Apply more activerecord filters to the tags_by_count_query, and then
# fetch the result with .count(Tag::COUNT_ARG).
#
# e.g., Tag.tags_by_count_query.where("topics.category_id = ?", category.id).count(Tag::COUNT_ARG)
def self.tags_by_count_query(opts = {})
q = Tag.joins("LEFT JOIN topic_tags ON tags.id = topic_tags.tag_id")
.joins("LEFT JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL")
.group("tags.id, tags.name")
.order('count_topics_id DESC')
q = q.limit(opts[:limit]) if opts[:limit]
q
end
def self.ensure_consistency!
update_topic_counts # topic_count counter cache can miscount
end
@ -43,7 +28,7 @@ class Tag < ActiveRecord::Base
SELECT COUNT(topics.id) AS topic_count, tags.id AS tag_id
FROM tags
LEFT JOIN topic_tags ON tags.id = topic_tags.tag_id
LEFT JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL
LEFT JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL AND topics.archetype != "private_message"
GROUP BY tags.id
) x
WHERE x.tag_id = t.id

View File

@ -1,22 +1,28 @@
class TopicTag < ActiveRecord::Base
belongs_to :topic
belongs_to :tag, counter_cache: "topic_count"
belongs_to :tag
after_create do
if topic.category_id
if stat = CategoryTagStat.where(tag_id: tag_id, category_id: topic.category_id).first
stat.increment!(:topic_count)
else
CategoryTagStat.create(tag_id: tag_id, category_id: topic.category_id, topic_count: 1)
if topic.archetype != Archetype.private_message
tag.increment!(:topic_count)
if topic.category_id
if stat = CategoryTagStat.where(tag_id: tag_id, category_id: topic.category_id).first
stat.increment!(:topic_count)
else
CategoryTagStat.create(tag_id: tag_id, category_id: topic.category_id, topic_count: 1)
end
end
end
end
after_destroy do
if topic.category_id
if stat = CategoryTagStat.where(tag_id: tag_id, category: topic.category_id).first
if topic.archetype != Archetype.private_message
if topic.category_id && stat = CategoryTagStat.where(tag_id: tag_id, category: topic.category_id).first
stat.topic_count == 1 ? stat.destroy : stat.decrement!(:topic_count)
end
tag.decrement!(:topic_count)
end
end
end

View File

@ -164,7 +164,7 @@ class PostRevisionSerializer < ApplicationSerializer
end
def include_tags_changes?
SiteSetting.tagging_enabled && previous["tags"] != current["tags"]
SiteSetting.tagging_enabled && previous["tags"] != current["tags"] && (!topic.private_message? || scope.can_tag_pms?)
end
protected
@ -206,7 +206,7 @@ class PostRevisionSerializer < ApplicationSerializer
latest_modifications["featured_link"] = [post.topic.featured_link]
end
if SiteSetting.tagging_enabled
if SiteSetting.tagging_enabled && (!topic.private_message? || scope.can_tag_pms?)
latest_modifications["tags"] = [post.topic.tags.map(&:name)]
end

View File

@ -1,12 +1,5 @@
class SearchTopicListItemSerializer < ListableTopicSerializer
attributes :tags,
:category_id
include TopicTagsMixin
def include_tags?
SiteSetting.tagging_enabled
end
def tags
object.tags.map(&:name)
end
attributes :category_id
end

View File

@ -21,6 +21,7 @@ class SiteSerializer < ApplicationSerializer
:topic_flag_types,
:can_create_tag,
:can_tag_topics,
:can_tag_pms,
:tags_filter_regexp,
:top_tags,
:wizard_required,
@ -106,11 +107,15 @@ class SiteSerializer < ApplicationSerializer
end
def can_create_tag
SiteSetting.tagging_enabled && scope.can_create_tag?
scope.can_create_tag?
end
def can_tag_topics
SiteSetting.tagging_enabled && scope.can_tag_topics?
scope.can_tag_topics?
end
def can_tag_pms
scope.can_tag_pms?
end
def include_tags_filter_regexp?

View File

@ -1,4 +1,5 @@
class SuggestedTopicSerializer < ListableTopicSerializer
include TopicTagsMixin
# need to embed so we have users
# front page json gets away without embedding
@ -7,21 +8,13 @@ class SuggestedTopicSerializer < ListableTopicSerializer
has_one :user, serializer: BasicUserSerializer, embed: :objects
end
attributes :archetype, :like_count, :views, :category_id, :tags, :featured_link, :featured_link_root_domain
attributes :archetype, :like_count, :views, :category_id, :featured_link, :featured_link_root_domain
has_many :posters, serializer: SuggestedPosterSerializer, embed: :objects
def posters
object.posters || []
end
def include_tags?
SiteSetting.tagging_enabled
end
def tags
object.tags.map(&:name)
end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end

View File

@ -1,4 +1,5 @@
class TopicListItemSerializer < ListableTopicSerializer
include TopicTagsMixin
attributes :views,
:like_count,
@ -10,7 +11,6 @@ class TopicListItemSerializer < ListableTopicSerializer
:pinned_globally,
:bookmarked_post_numbers,
:liked_post_numbers,
:tags,
:featured_link,
:featured_link_root_domain
@ -66,14 +66,6 @@ class TopicListItemSerializer < ListableTopicSerializer
object.association(:first_post).loaded?
end
def include_tags?
SiteSetting.tagging_enabled
end
def tags
object.tags.map(&:name)
end
def include_featured_link?
SiteSetting.topic_featured_link_enabled
end

View File

@ -0,0 +1,13 @@
module TopicTagsMixin
def self.included(klass)
klass.attributes :tags
end
def include_tags?
SiteSetting.tagging_enabled && (!object.private_message? || scope.can_tag_pms?)
end
def tags
object.tags.pluck(:name)
end
end

View File

@ -239,7 +239,7 @@ class TopicViewSerializer < ApplicationSerializer
end
def include_tags?
SiteSetting.tagging_enabled
SiteSetting.tagging_enabled && (!object.topic.private_message? || scope.can_tag_pms?)
end
def topic_timer

View File

@ -1609,6 +1609,7 @@ en:
tags_listed_by_group: "List tags by tag group on the Tags page (/tags)."
tag_style: "Visual style for tag badges."
staff_tags: "A list of tags that can only be applied by staff members"
allow_staff_to_tag_in_pm: "Allow staff members to tag any personal message"
min_trust_level_to_tag_topics: "Minimum trust level required to tag topics"
suppress_overlapping_tags_in_list: "If tags match exact words in topic titles, don't show the tag"
remove_muted_tags_from_latest: "Don't show topics tagged with muted tags in the latest topic list."

View File

@ -360,6 +360,7 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username }
get "#{root_path}/:username/messages/group/:group_name/archive" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username }
get "#{root_path}/:username/messages/tag/:tag_id" => "user_actions#private_messages", constraints: StaffConstraint.new
get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormat.username }, defaults: { format: :json }
get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormat.username, format: /(json|html)/ } }.merge(index == 1 ? { as: 'user' } : {}))
put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormat.username }, defaults: { format: :json }
@ -589,20 +590,20 @@ Discourse::Application.routes.draw do
resources :similar_topics
get "topics/feature_stats"
get "topics/created-by/:username" => "list#topics_by", as: "topics_by", constraints: { username: RouteFormat.username }
get "topics/private-messages/:username" => "list#private_messages", as: "topics_private_messages", constraints: { username: RouteFormat.username }
get "topics/private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", constraints: { username: RouteFormat.username }
get "topics/private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", constraints: { username: RouteFormat.username }
get "topics/private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", constraints: { username: RouteFormat.username }
get "topics/private-messages-group/:username/:group_name.json" => "list#private_messages_group", as: "topics_private_messages_group", constraints: {
username: RouteFormat.username,
group_name: RouteFormat.username
}
get "topics/private-messages-group/:username/:group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive", constraints: {
username: RouteFormat.username,
group_name: RouteFormat.username
}
scope "/topics", username: RouteFormat.username do
get "created-by/:username" => "list#topics_by", as: "topics_by"
get "private-messages/:username" => "list#private_messages", as: "topics_private_messages"
get "private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent"
get "private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive"
get "private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread"
get "private-messages-tag/:username/:tag_id.json" => "list#private_messages_tag", as: "topics_private_messages_tag", constraints: StaffConstraint.new
scope "/private-messages-group/:username", group_name: RouteFormat.username do
get ":group_name.json" => "list#private_messages_group", as: "topics_private_messages_group"
get ":group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive"
end
end
get 'embed/comments' => 'embed#comments'
get 'embed/count' => 'embed#count'

View File

@ -1560,6 +1560,8 @@ tags:
type: list
client: true
default: ''
allow_staff_to_tag_in_pm:
default: false
suppress_overlapping_tags_in_list:
default: false
client: true

View File

@ -4,10 +4,10 @@ module DiscourseTagging
TAGS_FILTER_REGEXP = /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<>
def self.tag_topic_by_names(topic, guardian, tag_names_arg, append: false)
if SiteSetting.tagging_enabled
if can_tag?(topic)
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || []
old_tag_names = topic.tags.map(&:name) || []
old_tag_names = topic.tags.pluck(:name) || []
new_tag_names = tag_names - old_tag_names
removed_tag_names = old_tag_names - tag_names

View File

@ -129,6 +129,12 @@ class Guardian
alias :can_see_flags? :can_moderate?
alias :can_close? :can_moderate?
def can_tag?(obj)
return false unless obj && obj.is_a?(Topic)
obj.private_message? ? can_tag_pms? : can_tag_topics?
end
def can_send_activation_email?(user)
user && is_staff? && !SiteSetting.must_approve_users?
end

View File

@ -5,7 +5,11 @@ module TagGuardian
end
def can_tag_topics?
user && user.has_trust_level?(SiteSetting.min_trust_level_to_tag_topics.to_i)
user && SiteSetting.tagging_enabled && user.has_trust_level?(SiteSetting.min_trust_level_to_tag_topics.to_i)
end
def can_tag_pms?
is_staff? && SiteSetting.tagging_enabled && SiteSetting.allow_staff_to_tag_in_pm
end
def can_admin_tags?

View File

@ -269,6 +269,14 @@ class TopicQuery
create_list(:private_messages, {}, list)
end
def list_private_messages_tag(user)
list = private_messages_for(user, :all)
tag_id = Tag.where('name ilike ?', @options[:tags][0]).pluck(:id).first
list = list.joins("JOIN topic_tags tt ON tt.topic_id = topics.id AND
tt.tag_id = #{tag_id}")
create_list(:private_messages, {}, list)
end
def list_category_topic_ids(category)
query = default_results(category: category.id)
pinned_ids = query.where('pinned_at IS NOT NULL AND category_id = ?', category.id).limit(nil).order('pinned_at DESC').pluck(:id)

View File

@ -15,51 +15,6 @@ describe Tag do
SiteSetting.min_trust_level_to_tag_topics = 0
end
describe '#tags_by_count_query' do
it "returns empty hash if nothing is tagged" do
expect(described_class.tags_by_count_query.count(Tag::COUNT_ARG)).to eq({})
end
context "with some tagged topics" do
before do
@topics = []
3.times { @topics << Fabricate(:topic) }
make_some_tags(count: 2)
@topics[0].tags << @tags[0]
@topics[0].tags << @tags[1]
@topics[1].tags << @tags[0]
end
it "returns tag names with topic counts in a hash" do
counts = described_class.tags_by_count_query.count(Tag::COUNT_ARG)
expect(counts[@tags[0].name]).to eq(2)
expect(counts[@tags[1].name]).to eq(1)
end
it "can be used to filter before doing the count" do
counts = described_class.tags_by_count_query.where("topics.id = ?", @topics[1].id).count(Tag::COUNT_ARG)
expect(counts).to eq(@tags[0].name => 1)
end
it "returns unused tags too" do
unused = Fabricate(:tag)
counts = described_class.tags_by_count_query.count(Tag::COUNT_ARG)
expect(counts[unused.name]).to eq(0)
end
it "doesn't include deleted topics in counts" do
deleted_topic_tag = Fabricate(:tag)
delete_topic = Fabricate(:topic)
post = Fabricate(:post, topic: delete_topic, user: delete_topic.user)
delete_topic.tags << deleted_topic_tag
PostDestroyer.new(Fabricate(:admin), post).destroy
counts = described_class.tags_by_count_query.count(Tag::COUNT_ARG)
expect(counts[deleted_topic_tag.name]).to eq(0)
end
end
end
describe '#top_tags' do
it "returns nothing if nothing has been tagged" do
make_some_tags(tag_a_topic: false)

View File

@ -1,6 +1,11 @@
require 'rails_helper'
describe TopicViewSerializer do
def serialize_topic(topic, user)
topic_view = TopicView.new(topic.id, user)
described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json
end
let(:topic) { Fabricate(:topic) }
let(:user) { Fabricate(:user) }
@ -12,8 +17,7 @@ describe TopicViewSerializer do
topic.update!(featured_link: featured_link)
SiteSetting.topic_featured_link_enabled = false
topic_view = TopicView.new(topic.id, user)
json = described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json
json = serialize_topic(topic, user)
expect(json[:featured_link]).to eq(nil)
expect(json[:featured_link_root_domain]).to eq(nil)
@ -24,8 +28,7 @@ describe TopicViewSerializer do
it 'should return the right attributes' do
topic.update!(featured_link: featured_link)
topic_view = TopicView.new(topic.id, user)
json = described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json
json = serialize_topic(topic, user)
expect(json[:featured_link]).to eq(featured_link)
expect(json[:featured_link_root_domain]).to eq('discourse.org')
@ -42,8 +45,7 @@ describe TopicViewSerializer do
describe 'when loading last chunk' do
it 'should include suggested topics' do
topic_view = TopicView.new(topic.id, user)
json = described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json
json = serialize_topic(topic, user)
expect(json[:suggested_topics].first.id).to eq(topic2.id)
end
@ -64,4 +66,33 @@ describe TopicViewSerializer do
end
end
end
let(:user) { Fabricate(:user) }
let(:moderator) { Fabricate(:moderator) }
let(:tag) { Fabricate(:tag) }
let(:pm) do
Fabricate(:private_message_topic, tags: [tag], topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: moderator),
Fabricate.build(:topic_allowed_user, user: user)
])
end
describe 'when tags added to private message topics' do
before do
SiteSetting.tagging_enabled = true
end
it "should not include the tag for normal users" do
json = serialize_topic(pm, user)
expect(json[:tags]).to eq(nil)
end
it "should include the tag for staff users" do
json = serialize_topic(pm, moderator)
expect(json[:tags]).to eq([tag.name])
json = serialize_topic(pm, Fabricate(:admin))
expect(json[:tags]).to eq([tag.name])
end
end
end