SECURITY: Default tags to show count of topics in unrestricted categories (#19916)
Currently, `Tag#topic_count` is a count of all regular topics regardless of whether the topic is in a read restricted category or not. As a result, any users can technically poll a sensitive tag to determine if a new topic is created in a category which the user has not excess to. We classify this as a minor leak in sensitive information. The following changes are introduced in this commit: 1. Introduce `Tag#public_topic_count` which only count topics which have been tagged with a given tag in public categories. 2. Rename `Tag#topic_count` to `Tag#staff_topic_count` which counts the same way as `Tag#topic_count`. In other words, it counts all topics tagged with a given tag regardless of the category the topic is in. The rename is also done so that we indicate that this column contains sensitive information. 3. Change all previous spots which relied on `Topic#topic_count` to rely on `Tag.topic_column_count(guardian)` which will return the right "topic count" column to use based on the current scope. 4. Introduce `SiteSetting.include_secure_categories_in_tag_counts` site setting to allow site administrators to always display the tag topics count using `Tag#staff_topic_count` instead.
This commit is contained in:
parent
4d2a95ffe6
commit
f122f24b35
|
@ -40,7 +40,7 @@ class TagsController < ::ApplicationController
|
|||
|
||||
if SiteSetting.tags_listed_by_group
|
||||
ungrouped_tags = Tag.where("tags.id NOT IN (SELECT tag_id FROM tag_group_memberships)")
|
||||
ungrouped_tags = ungrouped_tags.where("tags.topic_count > 0") unless show_all_tags
|
||||
ungrouped_tags = ungrouped_tags.used_tags_in_regular_topics(guardian) unless show_all_tags
|
||||
|
||||
grouped_tag_counts =
|
||||
TagGroup
|
||||
|
@ -51,18 +51,14 @@ class TagsController < ::ApplicationController
|
|||
{
|
||||
id: tag_group.id,
|
||||
name: tag_group.name,
|
||||
tags:
|
||||
self.class.tag_counts_json(
|
||||
tag_group.none_synonym_tags,
|
||||
show_pm_tags: guardian.can_tag_pms?,
|
||||
),
|
||||
tags: self.class.tag_counts_json(tag_group.none_synonym_tags, guardian),
|
||||
}
|
||||
end
|
||||
|
||||
@tags = self.class.tag_counts_json(ungrouped_tags, show_pm_tags: guardian.can_tag_pms?)
|
||||
@tags = self.class.tag_counts_json(ungrouped_tags, guardian)
|
||||
@extras = { tag_groups: grouped_tag_counts }
|
||||
else
|
||||
tags = show_all_tags ? Tag.all : Tag.where("tags.topic_count > 0")
|
||||
tags = show_all_tags ? Tag.all : Tag.used_tags_in_regular_topics(guardian)
|
||||
unrestricted_tags = DiscourseTagging.filter_visible(tags.where(target_tag_id: nil), guardian)
|
||||
|
||||
categories =
|
||||
|
@ -77,13 +73,14 @@ class TagsController < ::ApplicationController
|
|||
category_tags =
|
||||
self.class.tag_counts_json(
|
||||
DiscourseTagging.filter_visible(c.tags.where(target_tag_id: nil), guardian),
|
||||
guardian,
|
||||
)
|
||||
next if category_tags.empty?
|
||||
{ id: c.id, tags: category_tags }
|
||||
end
|
||||
.compact
|
||||
|
||||
@tags = self.class.tag_counts_json(unrestricted_tags, show_pm_tags: guardian.can_tag_pms?)
|
||||
@tags = self.class.tag_counts_json(unrestricted_tags, guardian)
|
||||
@extras = { categories: category_tag_counts }
|
||||
end
|
||||
|
||||
|
@ -264,7 +261,7 @@ class TagsController < ::ApplicationController
|
|||
tags_with_counts, filter_result_context =
|
||||
DiscourseTagging.filter_allowed_tags(guardian, **filter_params, with_context: true)
|
||||
|
||||
tags = self.class.tag_counts_json(tags_with_counts, show_pm_tags: guardian.can_tag_pms?)
|
||||
tags = self.class.tag_counts_json(tags_with_counts, guardian)
|
||||
|
||||
json_response = { results: tags }
|
||||
|
||||
|
@ -388,18 +385,22 @@ class TagsController < ::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def self.tag_counts_json(tags, show_pm_tags: true)
|
||||
def self.tag_counts_json(tags, guardian)
|
||||
show_pm_tags = guardian.can_tag_pms?
|
||||
target_tags = Tag.where(id: tags.map(&:target_tag_id).compact.uniq).select(:id, :name)
|
||||
|
||||
tags
|
||||
.map do |t|
|
||||
next if t.topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags
|
||||
topic_count = t.public_send(Tag.topic_count_column(guardian))
|
||||
|
||||
next if topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags
|
||||
|
||||
{
|
||||
id: t.name,
|
||||
text: t.name,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
count: t.topic_count,
|
||||
count: topic_count,
|
||||
pm_count: show_pm_tags ? t.pm_topic_count : 0,
|
||||
target_tag:
|
||||
t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil,
|
||||
|
|
|
@ -4,6 +4,10 @@ class Tag < ActiveRecord::Base
|
|||
include Searchable
|
||||
include HasDestroyedWebHook
|
||||
|
||||
self.ignored_columns = [
|
||||
"topic_count", # TODO(tgxworld): Remove on 1 July 2023
|
||||
]
|
||||
|
||||
RESERVED_TAGS = [
|
||||
"none",
|
||||
"constructor", # prevents issues with javascript's constructor of objects
|
||||
|
@ -25,11 +29,14 @@ class Tag < ActiveRecord::Base
|
|||
# tags that have never been used and don't belong to a tag group
|
||||
scope :unused,
|
||||
-> {
|
||||
where(topic_count: 0, pm_topic_count: 0).joins(
|
||||
where(staff_topic_count: 0, pm_topic_count: 0).joins(
|
||||
"LEFT JOIN tag_group_memberships tgm ON tags.id = tgm.tag_id",
|
||||
).where("tgm.tag_id IS NULL")
|
||||
}
|
||||
|
||||
scope :used_tags_in_regular_topics,
|
||||
->(guardian) { where("tags.#{Tag.topic_count_column(guardian)} > 0") }
|
||||
|
||||
scope :base_tags, -> { where(target_tag_id: nil) }
|
||||
|
||||
has_many :tag_users, dependent: :destroy # notification settings
|
||||
|
@ -62,7 +69,7 @@ class Tag < ActiveRecord::Base
|
|||
def self.update_topic_counts
|
||||
DB.exec <<~SQL
|
||||
UPDATE tags t
|
||||
SET topic_count = x.topic_count
|
||||
SET staff_topic_count = x.topic_count
|
||||
FROM (
|
||||
SELECT COUNT(topics.id) AS topic_count, tags.id AS tag_id
|
||||
FROM tags
|
||||
|
@ -73,7 +80,31 @@ class Tag < ActiveRecord::Base
|
|||
GROUP BY tags.id
|
||||
) x
|
||||
WHERE x.tag_id = t.id
|
||||
AND x.topic_count <> t.topic_count
|
||||
AND x.topic_count <> t.staff_topic_count
|
||||
SQL
|
||||
|
||||
DB.exec <<~SQL
|
||||
UPDATE tags t
|
||||
SET public_topic_count = x.topic_count
|
||||
FROM (
|
||||
WITH tags_with_public_topics AS (
|
||||
SELECT
|
||||
COUNT(topics.id) AS topic_count,
|
||||
tags.id AS tag_id
|
||||
FROM tags
|
||||
INNER JOIN topic_tags ON tags.id = topic_tags.tag_id
|
||||
INNER JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL AND topics.archetype != 'private_message'
|
||||
INNER JOIN categories ON categories.id = topics.category_id AND NOT categories.read_restricted
|
||||
GROUP BY tags.id
|
||||
)
|
||||
SELECT
|
||||
COALESCE(tags_with_public_topics.topic_count, 0 ) AS topic_count,
|
||||
tags.id AS tag_id
|
||||
FROM tags
|
||||
LEFT JOIN tags_with_public_topics ON tags_with_public_topics.tag_id = tags.id
|
||||
) x
|
||||
WHERE x.tag_id = t.id
|
||||
AND x.topic_count <> t.public_topic_count;
|
||||
SQL
|
||||
|
||||
DB.exec <<~SQL
|
||||
|
@ -97,19 +128,18 @@ class Tag < ActiveRecord::Base
|
|||
self.find_by("lower(name) = ?", name.downcase)
|
||||
end
|
||||
|
||||
def self.top_tags(limit_arg: nil, category: nil, guardian: nil)
|
||||
def self.top_tags(limit_arg: nil, category: nil, guardian: Guardian.new)
|
||||
# we add 1 to max_tags_in_filter_list to efficiently know we have more tags
|
||||
# than the limit. Frontend is responsible to enforce limit.
|
||||
limit = limit_arg || (SiteSetting.max_tags_in_filter_list + 1)
|
||||
scope_category_ids = (guardian || Guardian.new).allowed_category_ids
|
||||
|
||||
scope_category_ids = guardian.allowed_category_ids
|
||||
scope_category_ids &= ([category.id] + category.subcategories.pluck(:id)) if category
|
||||
|
||||
return [] if scope_category_ids.empty?
|
||||
|
||||
filter_sql =
|
||||
(
|
||||
if guardian&.is_staff?
|
||||
if guardian.is_staff?
|
||||
""
|
||||
else
|
||||
" AND tags.id IN (#{DiscourseTagging.visible_tags(guardian).select(:id).to_sql})"
|
||||
|
@ -130,6 +160,14 @@ class Tag < ActiveRecord::Base
|
|||
tag_names_with_counts.map { |row| row.tag_name }
|
||||
end
|
||||
|
||||
def self.topic_count_column(guardian)
|
||||
if guardian&.is_staff? || SiteSetting.include_secure_categories_in_tag_counts
|
||||
"staff_topic_count"
|
||||
else
|
||||
"public_topic_count"
|
||||
end
|
||||
end
|
||||
|
||||
def self.pm_tags(limit: 1000, guardian: nil, allowed_user: nil)
|
||||
return [] if allowed_user.blank? || !(guardian || Guardian.new).can_tag_pms?
|
||||
user_id = allowed_user.id
|
||||
|
@ -214,14 +252,15 @@ end
|
|||
#
|
||||
# Table name: tags
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# topic_count :integer default(0), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# pm_topic_count :integer default(0), not null
|
||||
# target_tag_id :integer
|
||||
# description :string
|
||||
# id :integer not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# pm_topic_count :integer default(0), not null
|
||||
# target_tag_id :integer
|
||||
# description :string
|
||||
# public_topic_count :integer default(0), not null
|
||||
# staff_topic_count :integer default(0), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -958,6 +958,7 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
def changed_to_category(new_category)
|
||||
return true if new_category.blank? || Category.exists?(topic_id: id)
|
||||
|
||||
if new_category.id == SiteSetting.uncategorized_category_id &&
|
||||
!SiteSetting.allow_uncategorized_topics
|
||||
return false
|
||||
|
@ -971,6 +972,15 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
if old_category
|
||||
Category.where(id: old_category.id).update_all("topic_count = topic_count - 1")
|
||||
|
||||
count =
|
||||
if old_category.read_restricted && !new_category.read_restricted
|
||||
1
|
||||
elsif !old_category.read_restricted && new_category.read_restricted
|
||||
-1
|
||||
end
|
||||
|
||||
Tag.update_counters(self.tags, { public_topic_count: count }) if count
|
||||
end
|
||||
|
||||
# when a topic changes category we may have to start watching it
|
||||
|
@ -1781,12 +1791,15 @@ class Topic < ActiveRecord::Base
|
|||
|
||||
def convert_to_public_topic(user, category_id: nil)
|
||||
public_topic = TopicConverter.new(self, user).convert_to_public_topic(category_id)
|
||||
Tag.update_counters(public_topic.tags, { public_topic_count: 1 }) if !category.read_restricted
|
||||
add_small_action(user, "public_topic") if public_topic
|
||||
public_topic
|
||||
end
|
||||
|
||||
def convert_to_private_message(user)
|
||||
read_restricted = category.read_restricted
|
||||
private_topic = TopicConverter.new(self, user).convert_to_private_message
|
||||
Tag.update_counters(private_topic.tags, { public_topic_count: -1 }) if !read_restricted
|
||||
add_small_action(user, "private_topic") if private_topic
|
||||
private_topic
|
||||
end
|
||||
|
|
|
@ -9,14 +9,18 @@ class TopicTag < ActiveRecord::Base
|
|||
if topic.archetype == Archetype.private_message
|
||||
tag.increment!(:pm_topic_count)
|
||||
else
|
||||
tag.increment!(:topic_count)
|
||||
counters_to_update = { staff_topic_count: 1 }
|
||||
|
||||
if topic.category_id
|
||||
if stat = CategoryTagStat.find_by(tag_id: tag_id, category_id: topic.category_id)
|
||||
stat.increment!(:topic_count)
|
||||
else
|
||||
CategoryTagStat.create(tag_id: tag_id, category_id: topic.category_id, topic_count: 1)
|
||||
end
|
||||
if Category.exists?(id: topic.category_id, read_restricted: false)
|
||||
counters_to_update[:public_topic_count] = 1
|
||||
end
|
||||
|
||||
Tag.update_counters(tag.id, counters_to_update)
|
||||
|
||||
if stat = CategoryTagStat.find_by(tag_id: tag_id, category_id: topic.category_id)
|
||||
stat.increment!(:topic_count)
|
||||
else
|
||||
CategoryTagStat.create!(tag_id: tag_id, category_id: topic.category_id, topic_count: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -27,12 +31,17 @@ class TopicTag < ActiveRecord::Base
|
|||
if topic.archetype == Archetype.private_message
|
||||
tag.decrement!(:pm_topic_count)
|
||||
else
|
||||
if topic.category_id &&
|
||||
stat = CategoryTagStat.find_by(tag_id: tag_id, category: topic.category_id)
|
||||
stat.topic_count == 1 ? stat.destroy : stat.decrement!(:topic_count)
|
||||
if stat = CategoryTagStat.find_by(tag_id: tag_id, category: topic.category_id)
|
||||
stat.topic_count == 1 ? stat.destroy! : stat.decrement!(:topic_count)
|
||||
end
|
||||
|
||||
tag.decrement!(:topic_count)
|
||||
counters_to_update = { staff_topic_count: -1 }
|
||||
|
||||
if Category.exists?(id: topic.category_id, read_restricted: false)
|
||||
counters_to_update[:public_topic_count] = -1
|
||||
end
|
||||
|
||||
Tag.update_counters(tag.id, counters_to_update)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32,7 +32,8 @@ module TopicTagsMixin
|
|||
if SiteSetting.tags_sort_alphabetically
|
||||
topic.tags.sort_by(&:name)
|
||||
else
|
||||
topic.tags.sort_by(&:topic_count).reverse
|
||||
topic_count_column = Tag.topic_count_column(scope)
|
||||
topic.tags.sort_by { |tag| tag.public_send(topic_count_column) }.reverse
|
||||
end
|
||||
)
|
||||
tags = tags.reject { |tag| scope.hidden_tag_names.include?(tag[:name]) } if !scope.is_staff?
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
module UserSidebarMixin
|
||||
def sidebar_tags
|
||||
topic_count_column = Tag.topic_count_column(scope)
|
||||
|
||||
object
|
||||
.visible_sidebar_tags(scope)
|
||||
.pluck(:name, :topic_count, :pm_topic_count)
|
||||
.pluck(:name, topic_count_column, :pm_topic_count)
|
||||
.reduce([]) do |tags, sidebar_tag|
|
||||
tags.push(name: sidebar_tag[0], pm_only: sidebar_tag[1] == 0 && sidebar_tag[2] > 0)
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class DetailedTagSerializer < TagSerializer
|
|||
has_many :categories, serializer: BasicCategorySerializer
|
||||
|
||||
def synonyms
|
||||
TagsController.tag_counts_json(object.synonyms)
|
||||
TagsController.tag_counts_json(object.synonyms, scope)
|
||||
end
|
||||
|
||||
def categories
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
class TagSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :topic_count, :staff, :description
|
||||
|
||||
def topic_count
|
||||
object.public_send(Tag.topic_count_column(scope))
|
||||
end
|
||||
|
||||
def staff
|
||||
DiscourseTagging.staff_tag_names.include?(name)
|
||||
end
|
||||
|
|
|
@ -12,26 +12,30 @@ class TagHashtagDataSource
|
|||
"tag"
|
||||
end
|
||||
|
||||
def self.tag_to_hashtag_item(tag)
|
||||
tag = Tag.new(tag.slice(:id, :name, :description).merge(topic_count: tag[:count])) if tag.is_a?(
|
||||
Hash,
|
||||
)
|
||||
def self.tag_to_hashtag_item(tag, guardian)
|
||||
topic_count_column = Tag.topic_count_column(guardian)
|
||||
|
||||
tag =
|
||||
Tag.new(
|
||||
tag.slice(:id, :name, :description).merge(topic_count_column => tag[:count]),
|
||||
) if tag.is_a?(Hash)
|
||||
|
||||
HashtagAutocompleteService::HashtagItem.new.tap do |item|
|
||||
item.text = tag.name
|
||||
item.secondary_text = "x#{tag.topic_count}"
|
||||
item.secondary_text = "x#{tag.public_send(topic_count_column)}"
|
||||
item.description = tag.description
|
||||
item.slug = tag.name
|
||||
item.relative_url = tag.url
|
||||
item.icon = icon
|
||||
end
|
||||
end
|
||||
private_class_method :tag_to_hashtag_item
|
||||
|
||||
def self.lookup(guardian, slugs)
|
||||
return [] if !SiteSetting.tagging_enabled
|
||||
DiscourseTagging
|
||||
.filter_visible(Tag.where_name(slugs), guardian)
|
||||
.map { |tag| tag_to_hashtag_item(tag) }
|
||||
.map { |tag| tag_to_hashtag_item(tag, guardian) }
|
||||
end
|
||||
|
||||
def self.search(
|
||||
|
@ -60,9 +64,9 @@ class TagHashtagDataSource
|
|||
)
|
||||
|
||||
TagsController
|
||||
.tag_counts_json(tags_with_counts)
|
||||
.tag_counts_json(tags_with_counts, guardian)
|
||||
.take(limit)
|
||||
.map { |tag| tag_to_hashtag_item(tag) }
|
||||
.map { |tag| tag_to_hashtag_item(tag, guardian) }
|
||||
end
|
||||
|
||||
def self.search_sort(search_results, _)
|
||||
|
@ -82,8 +86,8 @@ class TagHashtagDataSource
|
|||
)
|
||||
|
||||
TagsController
|
||||
.tag_counts_json(tags_with_counts)
|
||||
.tag_counts_json(tags_with_counts, guardian)
|
||||
.take(limit)
|
||||
.map { |tag| tag_to_hashtag_item(tag) }
|
||||
.map { |tag| tag_to_hashtag_item(tag, guardian) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1651,6 +1651,7 @@ en:
|
|||
content_security_policy_frame_ancestors: "Restrict who can embed this site in iframes via CSP. Control allowed hosts on <a href='%{base_path}/admin/customize/embedding'>Embedding</a>"
|
||||
content_security_policy_script_src: "Additional allowlisted script sources. The current host and CDN are included by default. See <a href='https://meta.discourse.org/t/mitigate-xss-attacks-with-content-security-policy/104243' target='_blank'>Mitigate XSS Attacks with Content Security Policy.</a>"
|
||||
invalidate_inactive_admin_email_after_days: "Admin accounts that have not visited the site in this number of days will need to re-validate their email address before logging in. Set to 0 to disable."
|
||||
include_secure_categories_in_tag_counts: "When enabled, count of topics for a tag will include topics that are in read restricted categories for all users. When disabled, normal users are only shown a count of topics for a tag where all the topics are in public categories."
|
||||
top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks"
|
||||
post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply"
|
||||
post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on."
|
||||
|
|
|
@ -1798,6 +1798,8 @@ security:
|
|||
suppress_secured_categories_from_admin:
|
||||
default: false
|
||||
hidden: true
|
||||
include_secure_categories_in_tag_counts:
|
||||
default: false
|
||||
|
||||
onebox:
|
||||
post_onebox_maxlength:
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPublicTopicCountToTags < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
add_column :tags, :public_topic_count, :integer, default: 0, null: false
|
||||
|
||||
execute <<~SQL
|
||||
UPDATE tags t
|
||||
SET public_topic_count = x.topic_count
|
||||
FROM (
|
||||
SELECT
|
||||
COUNT(topics.id) AS topic_count,
|
||||
tags.id AS tag_id
|
||||
FROM tags
|
||||
INNER JOIN topic_tags ON tags.id = topic_tags.tag_id
|
||||
INNER JOIN topics ON topics.id = topic_tags.topic_id AND topics.deleted_at IS NULL AND topics.archetype != 'private_message'
|
||||
INNER JOIN categories ON categories.id = topics.category_id AND NOT categories.read_restricted
|
||||
GROUP BY tags.id
|
||||
) x
|
||||
WHERE x.tag_id = t.id
|
||||
AND x.topic_count <> t.public_topic_count;
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddStaffTopicCountToTags < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
add_column :tags, :staff_topic_count, :integer, default: 0, null: false
|
||||
|
||||
execute <<~SQL
|
||||
UPDATE tags t
|
||||
SET staff_topic_count = x.topic_count
|
||||
FROM (
|
||||
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
|
||||
AND topics.archetype != 'private_message'
|
||||
GROUP BY tags.id
|
||||
) x
|
||||
WHERE x.tag_id = t.id
|
||||
AND x.topic_count <> t.staff_topic_count
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "migration/column_dropper"
|
||||
|
||||
class RemoveTopicCountFromTags < ActiveRecord::Migration[7.0]
|
||||
DROPPED_COLUMNS ||= { tags: %i[topic_count] }
|
||||
|
||||
def up
|
||||
DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) }
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
|
@ -323,17 +323,19 @@ module DiscourseTagging
|
|||
|
||||
outer_join = category.nil? || category.allow_global_tags || !category_has_restricted_tags
|
||||
|
||||
topic_count_column = Tag.topic_count_column(guardian)
|
||||
|
||||
distinct_clause =
|
||||
if opts[:order_popularity]
|
||||
"DISTINCT ON (topic_count, name)"
|
||||
"DISTINCT ON (#{topic_count_column}, name)"
|
||||
elsif opts[:order_search_results] && opts[:term].present?
|
||||
"DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)"
|
||||
"DISTINCT ON (lower(name) = lower(:cleaned_term), #{topic_count_column}, name)"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
sql << <<~SQL
|
||||
SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count, t.description,
|
||||
SELECT #{distinct_clause} t.id, t.name, t.#{topic_count_column}, t.pm_topic_count, t.description,
|
||||
tgr.tgm_id as tgm_id, tgr.tag_group_id as tag_group_id, tgr.parent_tag_id as parent_tag_id,
|
||||
tgr.one_per_topic as one_per_topic, t.target_tag_id
|
||||
FROM tags t
|
||||
|
@ -479,9 +481,9 @@ module DiscourseTagging
|
|||
end
|
||||
|
||||
if opts[:order_popularity]
|
||||
builder.order_by("topic_count DESC, name")
|
||||
builder.order_by("#{topic_count_column} DESC, name")
|
||||
elsif opts[:order_search_results] && !term.blank?
|
||||
builder.order_by("lower(name) = lower(:cleaned_term) DESC, topic_count DESC, name")
|
||||
builder.order_by("lower(name) = lower(:cleaned_term) DESC, #{topic_count_column} DESC, name")
|
||||
end
|
||||
|
||||
result = builder.query(builder_params).uniq { |t| t.id }
|
||||
|
|
|
@ -245,7 +245,8 @@ class TopicView
|
|||
@topic.category
|
||||
title += " - #{@topic.category.name}"
|
||||
elsif SiteSetting.tagging_enabled && @topic.tags.exists?
|
||||
title += " - #{@topic.tags.order("tags.topic_count DESC").first.name}"
|
||||
title +=
|
||||
" - #{@topic.tags.order("tags.#{Tag.topic_count_column(@guardian)} DESC").first.name}"
|
||||
end
|
||||
end
|
||||
title
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "Updating tag counts" do
|
||||
fab!(:tag1) { Fabricate(:tag) }
|
||||
fab!(:tag2) { Fabricate(:tag) }
|
||||
fab!(:group) { Fabricate(:group) }
|
||||
fab!(:public_category) { Fabricate(:category) }
|
||||
fab!(:public_category2) { Fabricate(:category) }
|
||||
fab!(:private_category) { Fabricate(:private_category, group: group) }
|
||||
fab!(:private_category2) { Fabricate(:private_category, group: group) }
|
||||
|
||||
fab!(:topic_in_public_category) do
|
||||
Fabricate(:topic, category: public_category, tags: [tag1, tag2]).tap do |topic|
|
||||
Fabricate(:post, topic: topic)
|
||||
end
|
||||
end
|
||||
|
||||
fab!(:topic_in_private_category) do
|
||||
Fabricate(:topic, category: private_category, tags: [tag1, tag2]).tap do |topic|
|
||||
Fabricate(:post, topic: topic)
|
||||
end
|
||||
end
|
||||
|
||||
fab!(:private_message) do
|
||||
topic = Fabricate(:private_message_post).topic
|
||||
topic.update!(tags: [tag1, tag2])
|
||||
topic
|
||||
end
|
||||
|
||||
before do
|
||||
expect(tag1.public_topic_count).to eq(1)
|
||||
expect(tag1.staff_topic_count).to eq(2)
|
||||
expect(tag1.pm_topic_count).to eq(1)
|
||||
expect(tag2.reload.public_topic_count).to eq(1)
|
||||
expect(tag2.staff_topic_count).to eq(2)
|
||||
expect(tag2.pm_topic_count).to eq(1)
|
||||
end
|
||||
|
||||
it "should decrease Tag#public_topic_count for all tags when topic's category is changed from a public category to a read restricted category" do
|
||||
expect { topic_in_public_category.change_category_to_id(private_category.id) }.to change {
|
||||
tag1.reload.public_topic_count
|
||||
}.by(-1).and change { tag2.reload.public_topic_count }.by(-1)
|
||||
end
|
||||
|
||||
it "should increase Tag#public_topic_count for all tags when topic's category is changed from a read restricted category to a public category" do
|
||||
expect { topic_in_private_category.change_category_to_id(public_category.id) }.to change {
|
||||
tag1.reload.public_topic_count
|
||||
}.by(1).and change { tag2.reload.public_topic_count }.by(1)
|
||||
end
|
||||
|
||||
it "should not change Tag#public_topic_count for all tags when topic's category is changed from a public category to another public category" do
|
||||
expect do
|
||||
topic_in_public_category.change_category_to_id(public_category2.id)
|
||||
end.to not_change { tag1.reload.public_topic_count }.and not_change {
|
||||
tag2.reload.public_topic_count
|
||||
}
|
||||
end
|
||||
|
||||
it "should not change Tag#public_topic_count for all tags when topic's category is changed from a read restricted category to another read restricted category" do
|
||||
expect do
|
||||
topic_in_private_category.change_category_to_id(private_category2.id)
|
||||
end.to not_change { tag1.reload.public_topic_count }.and not_change {
|
||||
tag2.reload.public_topic_count
|
||||
}
|
||||
end
|
||||
|
||||
it "increases Tag#public_topic_count for all tags when topic is converted from private message to a regular topic in a public category" do
|
||||
expect do
|
||||
private_message.convert_to_public_topic(
|
||||
Discourse.system_user,
|
||||
category_id: public_category.id,
|
||||
)
|
||||
end.to change { tag1.reload.public_topic_count }.by(1).and change {
|
||||
tag2.reload.public_topic_count
|
||||
}.by(1)
|
||||
end
|
||||
|
||||
it "should not change Tag#public_topic_count for all tags when topic is converted from private message to a regular topic in a read restricted category" do
|
||||
expect do
|
||||
private_message.convert_to_public_topic(
|
||||
Discourse.system_user,
|
||||
category_id: private_category.id,
|
||||
)
|
||||
end.to not_change { tag1.reload.public_topic_count }.and not_change {
|
||||
tag2.reload.public_topic_count
|
||||
}
|
||||
end
|
||||
|
||||
it "should decrease Tag#public_topic_count for all tags when regular topic in public category is converted to a private message" do
|
||||
expect do
|
||||
topic_in_public_category.convert_to_private_message(Discourse.system_user)
|
||||
end.to change { tag1.reload.public_topic_count }.by(-1).and change {
|
||||
tag2.reload.public_topic_count
|
||||
}.by(-1)
|
||||
end
|
||||
|
||||
it "should not change Tag#public_topic_count for all tags when regular topic in read restricted category is converted to a private message" do
|
||||
expect do
|
||||
topic_in_private_category.convert_to_private_message(Discourse.system_user)
|
||||
end.to not_change { tag1.reload.public_topic_count }.and not_change {
|
||||
tag2.reload.public_topic_count
|
||||
}
|
||||
end
|
||||
end
|
|
@ -641,9 +641,11 @@ RSpec.describe DiscourseTagging do
|
|||
|
||||
it "user does not get an error when editing their topic with a hidden tag" do
|
||||
PostRevisor.new(post).revise!(admin, raw: post.raw, tags: [hidden_tag.name])
|
||||
|
||||
expect(
|
||||
PostRevisor.new(post).revise!(topic.user, raw: post.raw + " edit", tags: []),
|
||||
).to be_truthy
|
||||
|
||||
expect(topic.reload.tags).to eq([hidden_tag])
|
||||
end
|
||||
end
|
||||
|
@ -967,8 +969,16 @@ RSpec.describe DiscourseTagging do
|
|||
topic = Fabricate(:topic, tags: [tag2])
|
||||
expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name])).to eq(true)
|
||||
expect_same_tag_names(topic.reload.tags, [tag1])
|
||||
expect(tag1.reload.topic_count).to eq(1)
|
||||
expect(tag2.reload.topic_count).to eq(0)
|
||||
|
||||
tag1.reload
|
||||
|
||||
expect(tag1.public_topic_count).to eq(1)
|
||||
expect(tag1.staff_topic_count).to eq(1)
|
||||
|
||||
tag2.reload
|
||||
|
||||
expect(tag2.public_topic_count).to eq(0)
|
||||
expect(tag2.staff_topic_count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -750,8 +750,8 @@ RSpec.describe TopicView do
|
|||
end
|
||||
|
||||
describe "page_title" do
|
||||
fab!(:tag1) { Fabricate(:tag) }
|
||||
fab!(:tag2) { Fabricate(:tag, topic_count: 2) }
|
||||
fab!(:tag1) { Fabricate(:tag, staff_topic_count: 0, public_topic_count: 0) }
|
||||
fab!(:tag2) { Fabricate(:tag, staff_topic_count: 2, public_topic_count: 2) }
|
||||
fab!(:op_post) { Fabricate(:post, topic: topic) }
|
||||
fab!(:post1) { Fabricate(:post, topic: topic) }
|
||||
fab!(:whisper) { Fabricate(:post, topic: topic, post_type: Post.types[:whisper]) }
|
||||
|
|
|
@ -202,29 +202,101 @@ RSpec.describe Tag do
|
|||
end
|
||||
end
|
||||
|
||||
describe "topic counts" do
|
||||
describe ".ensure_consistency!" do
|
||||
it "should exclude private message topics" do
|
||||
topic
|
||||
Fabricate(:private_message_topic, tags: [tag])
|
||||
Tag.ensure_consistency!
|
||||
tag.reload
|
||||
expect(tag.topic_count).to eq(1)
|
||||
expect(tag.staff_topic_count).to eq(1)
|
||||
expect(tag.public_topic_count).to eq(1)
|
||||
end
|
||||
|
||||
it "should update Tag#topic_count and Tag#public_topic_count correctly" do
|
||||
tag = Fabricate(:tag, name: "tag1")
|
||||
tag2 = Fabricate(:tag, name: "tag2")
|
||||
tag3 = Fabricate(:tag, name: "tag3")
|
||||
group = Fabricate(:group)
|
||||
category = Fabricate(:category)
|
||||
private_category = Fabricate(:private_category, group: group)
|
||||
private_category2 = Fabricate(:private_category, group: group)
|
||||
|
||||
_topic_with_tag = Fabricate(:topic, category: category, tags: [tag])
|
||||
|
||||
_topic_with_tag_in_private_category =
|
||||
Fabricate(:topic, category: private_category, tags: [tag])
|
||||
|
||||
_topic_with_tag2_in_private_category2 =
|
||||
Fabricate(:topic, category: private_category2, tags: [tag2])
|
||||
|
||||
tag.update!(staff_topic_count: 123, public_topic_count: 456)
|
||||
tag2.update!(staff_topic_count: 123, public_topic_count: 456)
|
||||
tag3.update!(staff_topic_count: 123, public_topic_count: 456)
|
||||
|
||||
Tag.ensure_consistency!
|
||||
|
||||
tag.reload
|
||||
tag2.reload
|
||||
tag3.reload
|
||||
|
||||
expect(tag.staff_topic_count).to eq(2)
|
||||
expect(tag.public_topic_count).to eq(1)
|
||||
expect(tag2.staff_topic_count).to eq(1)
|
||||
expect(tag2.public_topic_count).to eq(0)
|
||||
expect(tag3.staff_topic_count).to eq(0)
|
||||
expect(tag3.public_topic_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "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),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "used_publically",
|
||||
staff_topic_count: 2,
|
||||
public_topic_count: 2,
|
||||
pm_topic_count: 0,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "used_privately",
|
||||
staff_topic_count: 0,
|
||||
public_topic_count: 0,
|
||||
pm_topic_count: 3,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "used_everywhere",
|
||||
staff_topic_count: 0,
|
||||
public_topic_count: 0,
|
||||
pm_topic_count: 3,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "unused1",
|
||||
staff_topic_count: 0,
|
||||
public_topic_count: 0,
|
||||
pm_topic_count: 0,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "unused2",
|
||||
staff_topic_count: 0,
|
||||
public_topic_count: 0,
|
||||
pm_topic_count: 0,
|
||||
),
|
||||
]
|
||||
end
|
||||
|
||||
let(:tag_in_group) do
|
||||
Fabricate(:tag, name: "unused_in_group", topic_count: 0, pm_topic_count: 0)
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "unused_in_group",
|
||||
public_topic_count: 0,
|
||||
staff_topic_count: 0,
|
||||
pm_topic_count: 0,
|
||||
)
|
||||
end
|
||||
let!(:tag_group) { Fabricate(:tag_group, tag_names: [tag_in_group.name]) }
|
||||
|
||||
|
@ -292,4 +364,22 @@ RSpec.describe Tag do
|
|||
expect(category.reload.tags).to include(tag2)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".topic_count_column" do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
|
||||
it "returns 'staff_topic_count' when user is staff" do
|
||||
expect(Tag.topic_count_column(Guardian.new(admin))).to eq("staff_topic_count")
|
||||
end
|
||||
|
||||
it "returns 'public_topic_count' when user is not staff" do
|
||||
expect(Tag.topic_count_column(Guardian.new(user))).to eq("public_topic_count")
|
||||
end
|
||||
|
||||
it "returns 'staff_topic_count' when user is not staff but `include_secure_categories_in_tag_counts` site setting is enabled" do
|
||||
SiteSetting.include_secure_categories_in_tag_counts = true
|
||||
|
||||
expect(Tag.topic_count_column(Guardian.new(user))).to eq("staff_topic_count")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,33 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe TopicTag do
|
||||
fab!(:group) { Fabricate(:group) }
|
||||
fab!(:private_category) { Fabricate(:private_category, group: group) }
|
||||
fab!(:topic) { Fabricate(:topic) }
|
||||
fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category) }
|
||||
fab!(:tag) { Fabricate(:tag) }
|
||||
let(:topic_tag) { Fabricate(:topic_tag, topic: topic, tag: tag) }
|
||||
|
||||
describe "#after_create" do
|
||||
it "tag topic_count should be increased" do
|
||||
expect { topic_tag }.to change(tag, :topic_count).by(1)
|
||||
it "should increase Tag#staff_topic_count and Tag#public_topic_count for a regular topic in a public category" do
|
||||
expect { topic_tag }.to change { tag.reload.staff_topic_count }.by(1).and change {
|
||||
tag.reload.public_topic_count
|
||||
}.by(1)
|
||||
end
|
||||
|
||||
it "tag topic_count should not be increased" do
|
||||
it "should only increase Tag#staff_topic_count for a regular topic in a read restricted category" do
|
||||
expect { Fabricate(:topic_tag, topic: topic_in_private_category, tag: tag) }.to change {
|
||||
tag.reload.staff_topic_count
|
||||
}.by(1)
|
||||
|
||||
expect(tag.reload.public_topic_count).to eq(0)
|
||||
end
|
||||
|
||||
it "should increase Tag#pm_topic_count for a private message topic" do
|
||||
topic.archetype = Archetype.private_message
|
||||
|
||||
expect { topic_tag }.not_to change(tag, :topic_count)
|
||||
expect { topic_tag }.to change { tag.reload.pm_topic_count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#after_destroy" do
|
||||
it "tag topic_count should be decreased" do
|
||||
it "should decrease Tag#staff_topic_count and Tag#public_topic_count for a regular topic in a public category" do
|
||||
topic_tag
|
||||
expect { topic_tag.destroy }.to change(tag, :topic_count).by(-1)
|
||||
|
||||
expect { topic_tag.destroy! }.to change { tag.reload.staff_topic_count }.by(-1).and change {
|
||||
tag.reload.public_topic_count
|
||||
}.by(-1)
|
||||
end
|
||||
|
||||
it "tag topic_count should not be decreased" do
|
||||
it "should only decrease Topic#topic_count for a regular topic in a read restricted category" do
|
||||
topic_tag = Fabricate(:topic_tag, topic: topic_in_private_category, tag: tag)
|
||||
|
||||
expect { topic_tag.destroy! }.to change { tag.reload.staff_topic_count }.by(-1)
|
||||
expect(tag.reload.public_topic_count).to eq(0)
|
||||
end
|
||||
|
||||
it "should decrease Tag#pm_topic_count for a private message topic" do
|
||||
topic.archetype = Archetype.private_message
|
||||
topic_tag
|
||||
|
||||
expect { topic_tag.destroy }.not_to change(tag, :topic_count)
|
||||
expect { topic_tag.destroy! }.to change { tag.reload.pm_topic_count }.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,18 +12,43 @@ RSpec.describe TagsController do
|
|||
|
||||
describe "#index" do
|
||||
fab!(:test_tag) { Fabricate(:tag, name: "test") }
|
||||
fab!(:topic_tag) { Fabricate(:tag, name: "topic-test", topic_count: 1) }
|
||||
fab!(:topic_tag) do
|
||||
Fabricate(:tag, name: "topic-test", public_topic_count: 1, staff_topic_count: 1)
|
||||
end
|
||||
fab!(:synonym) { Fabricate(:tag, name: "synonym", target_tag: topic_tag) }
|
||||
|
||||
shared_examples "successfully retrieve tags with topic_count > 0" do
|
||||
it "should return the right response" do
|
||||
shared_examples "retrieves the right tags" do
|
||||
it "retrieves all tags as a staff user" do
|
||||
sign_in(admin)
|
||||
|
||||
get "/tags.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
tags = response.parsed_body["tags"]
|
||||
|
||||
expect(tags[0]["name"]).to eq(test_tag.name)
|
||||
expect(tags[0]["count"]).to eq(0)
|
||||
expect(tags[0]["pm_count"]).to eq(0)
|
||||
|
||||
expect(tags[1]["name"]).to eq(topic_tag.name)
|
||||
expect(tags[1]["count"]).to eq(1)
|
||||
expect(tags[1]["pm_count"]).to eq(0)
|
||||
end
|
||||
|
||||
it "only retrieve tags that have been used in public topics for non-staff user" do
|
||||
sign_in(user)
|
||||
|
||||
get "/tags.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
tags = response.parsed_body["tags"]
|
||||
expect(tags.length).to eq(1)
|
||||
expect(tags[0]["text"]).to eq("topic-test")
|
||||
|
||||
expect(tags[0]["name"]).to eq(topic_tag.name)
|
||||
expect(tags[0]["count"]).to eq(1)
|
||||
expect(tags[0]["pm_count"]).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -76,7 +101,7 @@ RSpec.describe TagsController do
|
|||
|
||||
context "with tags_listed_by_group enabled" do
|
||||
before { SiteSetting.tags_listed_by_group = true }
|
||||
include_examples "successfully retrieve tags with topic_count > 0"
|
||||
include_examples "retrieves the right tags"
|
||||
|
||||
it "works for tags in groups" do
|
||||
tag_group = Fabricate(:tag_group, tags: [test_tag, topic_tag, synonym])
|
||||
|
@ -136,20 +161,7 @@ RSpec.describe TagsController do
|
|||
|
||||
context "with tags_listed_by_group disabled" do
|
||||
before { SiteSetting.tags_listed_by_group = false }
|
||||
include_examples "successfully retrieve tags with topic_count > 0"
|
||||
end
|
||||
|
||||
context "when user can admin tags" do
|
||||
it "successfully retrieve all tags" do
|
||||
sign_in(admin)
|
||||
|
||||
get "/tags.json"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
tags = response.parsed_body["tags"]
|
||||
expect(tags.length).to eq(2)
|
||||
end
|
||||
include_examples "retrieves the right tags"
|
||||
end
|
||||
|
||||
context "with hidden tags" do
|
||||
|
@ -763,10 +775,10 @@ RSpec.describe TagsController do
|
|||
expect(response.parsed_body["results"].map { |j| j["id"] }.sort).to eq(%w[stuff stumped])
|
||||
end
|
||||
|
||||
it "returns tags ordered by topic_count, and prioritises exact matches" do
|
||||
Fabricate(:tag, name: "tag1", topic_count: 10)
|
||||
Fabricate(:tag, name: "tag2", topic_count: 100)
|
||||
Fabricate(:tag, name: "tag", topic_count: 1)
|
||||
it "returns tags ordered by public_topic_count, and prioritises exact matches" do
|
||||
Fabricate(:tag, name: "tag1", public_topic_count: 10, staff_topic_count: 10)
|
||||
Fabricate(:tag, name: "tag2", public_topic_count: 100, staff_topic_count: 100)
|
||||
Fabricate(:tag, name: "tag", public_topic_count: 1, staff_topic_count: 1)
|
||||
|
||||
get "/tags/filter/search.json", params: { q: "tag", limit: 2 }
|
||||
expect(response.status).to eq(200)
|
||||
|
@ -976,11 +988,41 @@ RSpec.describe TagsController do
|
|||
context "with some tags" 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),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "used_publically",
|
||||
public_topic_count: 2,
|
||||
staff_topic_count: 2,
|
||||
pm_topic_count: 0,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "used_privately",
|
||||
public_topic_count: 0,
|
||||
staff_topic_count: 0,
|
||||
pm_topic_count: 3,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "used_everywhere",
|
||||
public_topic_count: 0,
|
||||
staff_topic_count: 0,
|
||||
pm_topic_count: 3,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "unused1",
|
||||
public_topic_count: 0,
|
||||
staff_topic_count: 0,
|
||||
pm_topic_count: 0,
|
||||
),
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "unused2",
|
||||
public_topic_count: 0,
|
||||
staff_topic_count: 0,
|
||||
pm_topic_count: 0,
|
||||
),
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe TagSerializer do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
fab!(:tag) { Fabricate(:tag) }
|
||||
fab!(:group) { Fabricate(:group) }
|
||||
fab!(:private_category) { Fabricate(:private_category, group: group) }
|
||||
fab!(:topic_in_public_category) { Fabricate(:topic, tags: [tag]) }
|
||||
fab!(:topic_in_private_category) { Fabricate(:topic, category: private_category, tags: [tag]) }
|
||||
|
||||
describe "#topic_count" do
|
||||
it "should return the value of `Tag#public_topic_count` for a non-staff user" do
|
||||
serialized = described_class.new(tag, scope: Guardian.new(user), root: false).as_json
|
||||
|
||||
expect(serialized[:topic_count]).to eq(1)
|
||||
end
|
||||
|
||||
it "should return the vavlue of `Tag#topic_count` for a staff user" do
|
||||
serialized = described_class.new(tag, scope: Guardian.new(admin), root: false).as_json
|
||||
|
||||
expect(serialized[:topic_count]).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -279,9 +279,33 @@ RSpec.describe TopicViewSerializer do
|
|||
end
|
||||
|
||||
describe "tags order" do
|
||||
fab!(:tag1) { Fabricate(:tag, name: "ctag", description: "c description", topic_count: 5) }
|
||||
fab!(:tag2) { Fabricate(:tag, name: "btag", description: "b description", topic_count: 9) }
|
||||
fab!(:tag3) { Fabricate(:tag, name: "atag", description: "a description", topic_count: 3) }
|
||||
fab!(:tag1) do
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "ctag",
|
||||
description: "c description",
|
||||
staff_topic_count: 5,
|
||||
public_topic_count: 5,
|
||||
)
|
||||
end
|
||||
fab!(:tag2) do
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "btag",
|
||||
description: "b description",
|
||||
staff_topic_count: 9,
|
||||
public_topic_count: 9,
|
||||
)
|
||||
end
|
||||
fab!(:tag3) do
|
||||
Fabricate(
|
||||
:tag,
|
||||
name: "atag",
|
||||
description: "a description",
|
||||
staff_topic_count: 3,
|
||||
public_topic_count: 3,
|
||||
)
|
||||
end
|
||||
|
||||
before do
|
||||
topic.tags << tag1
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
RSpec.describe HashtagAutocompleteService do
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
fab!(:category1) { Fabricate(:category, name: "The Book Club", slug: "the-book-club") }
|
||||
fab!(:tag1) { Fabricate(:tag, name: "great-books", topic_count: 22) }
|
||||
fab!(:tag1) do
|
||||
Fabricate(:tag, name: "great-books", staff_topic_count: 22, public_topic_count: 22)
|
||||
end
|
||||
fab!(:topic1) { Fabricate(:topic) }
|
||||
let(:guardian) { Guardian.new(user) }
|
||||
|
||||
|
@ -68,7 +70,7 @@ RSpec.describe HashtagAutocompleteService do
|
|||
end
|
||||
|
||||
it "includes the tag count" do
|
||||
tag1.update!(topic_count: 78)
|
||||
tag1.update!(staff_topic_count: 78, public_topic_count: 78)
|
||||
expect(subject.search("book", %w[tag category]).map(&:text)).to eq(
|
||||
["great-books", "The Book Club"],
|
||||
)
|
||||
|
@ -149,8 +151,8 @@ RSpec.describe HashtagAutocompleteService do
|
|||
category6 = Fabricate(:category, name: "Book Reviews", slug: "book-reviews")
|
||||
Fabricate(:category, name: "Good Books", slug: "book", parent_category: category6)
|
||||
|
||||
Fabricate(:tag, name: "bookmania", topic_count: 15)
|
||||
Fabricate(:tag, name: "awful-books", topic_count: 56)
|
||||
Fabricate(:tag, name: "bookmania", staff_topic_count: 15, public_topic_count: 15)
|
||||
Fabricate(:tag, name: "awful-books", staff_topic_count: 56, public_topic_count: 56)
|
||||
|
||||
expect(subject.search("book", %w[category tag]).map(&:ref)).to eq(
|
||||
[
|
||||
|
@ -220,9 +222,13 @@ RSpec.describe HashtagAutocompleteService do
|
|||
end
|
||||
fab!(:category4) { Fabricate(:category, name: "Bookworld", slug: "book", topic_count: 56) }
|
||||
fab!(:category5) { Fabricate(:category, name: "Media", slug: "media", topic_count: 446) }
|
||||
fab!(:tag2) { Fabricate(:tag, name: "mid-books", topic_count: 33) }
|
||||
fab!(:tag3) { Fabricate(:tag, name: "terrible-books", topic_count: 2) }
|
||||
fab!(:tag4) { Fabricate(:tag, name: "book", topic_count: 1) }
|
||||
fab!(:tag2) do
|
||||
Fabricate(:tag, name: "mid-books", staff_topic_count: 33, public_topic_count: 33)
|
||||
end
|
||||
fab!(:tag3) do
|
||||
Fabricate(:tag, name: "terrible-books", staff_topic_count: 2, public_topic_count: 2)
|
||||
end
|
||||
fab!(:tag4) { Fabricate(:tag, name: "book", staff_topic_count: 1, public_topic_count: 1) }
|
||||
|
||||
it "returns the 'most polular' categories and tags (based on topic_count) that the user can access" do
|
||||
category1.update!(read_restricted: true)
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe TagHashtagDataSource do
|
||||
fab!(:tag1) { Fabricate(:tag, name: "fact", topic_count: 0) }
|
||||
fab!(:tag2) { Fabricate(:tag, name: "factor", topic_count: 5) }
|
||||
fab!(:tag3) { Fabricate(:tag, name: "factory", topic_count: 4) }
|
||||
fab!(:tag4) { Fabricate(:tag, name: "factorio", topic_count: 3) }
|
||||
fab!(:tag5) { Fabricate(:tag, name: "factz", topic_count: 1) }
|
||||
fab!(:tag1) { Fabricate(:tag, name: "fact", public_topic_count: 0) }
|
||||
fab!(:tag2) { Fabricate(:tag, name: "factor", public_topic_count: 5) }
|
||||
fab!(:tag3) { Fabricate(:tag, name: "factory", public_topic_count: 4) }
|
||||
fab!(:tag4) { Fabricate(:tag, name: "factorio", public_topic_count: 3) }
|
||||
fab!(:tag5) { Fabricate(:tag, name: "factz", public_topic_count: 1) }
|
||||
fab!(:user) { Fabricate(:user) }
|
||||
let(:guardian) { Guardian.new(user) }
|
||||
|
||||
describe "#search" do
|
||||
it "orders tag results by exact search match, then topic count, then name" do
|
||||
it "orders tag results by exact search match, then public topic count, then name" do
|
||||
expect(described_class.search(guardian, "fact", 5).map(&:slug)).to eq(
|
||||
%w[fact factor factory factorio factz],
|
||||
)
|
||||
|
@ -31,7 +31,7 @@ RSpec.describe TagHashtagDataSource do
|
|||
)
|
||||
end
|
||||
|
||||
it "includes the topic count for the text of the tag in secondary text" do
|
||||
it "includes the public topic count for the text of the tag in secondary text" do
|
||||
expect(described_class.search(guardian, "fact", 5).map(&:secondary_text)).to eq(
|
||||
%w[x0 x5 x4 x3 x1],
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ RSpec.describe TagHashtagDataSource do
|
|||
end
|
||||
|
||||
describe "#search_without_term" do
|
||||
it "returns distinct tags sorted by topic_count" do
|
||||
it "returns distinct tags sorted by public topic count" do
|
||||
expect(described_class.search_without_term(guardian, 5).map(&:slug)).to eq(
|
||||
%w[factor factory factorio factz fact],
|
||||
)
|
||||
|
|
|
@ -65,7 +65,9 @@ RSpec.shared_examples "User Sidebar Serializer Attributes" do |serializer_klass|
|
|||
|
||||
describe "#sidebar_tags" do
|
||||
fab!(:tag) { Fabricate(:tag, name: "foo") }
|
||||
fab!(:pm_tag) { Fabricate(:tag, name: "bar", pm_topic_count: 5, topic_count: 0) }
|
||||
fab!(:pm_tag) do
|
||||
Fabricate(:tag, name: "bar", pm_topic_count: 5, staff_topic_count: 0, public_topic_count: 0)
|
||||
end
|
||||
fab!(:hidden_tag) { Fabricate(:tag, name: "secret") }
|
||||
fab!(:staff_tag_group) do
|
||||
Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: ["secret"])
|
||||
|
|
|
@ -10,8 +10,8 @@ describe "Using #hashtag autocompletion to search for and lookup categories and
|
|||
fab!(:category2) do
|
||||
Fabricate(:category, name: "Other Category", slug: "other-cat", topic_count: 23)
|
||||
end
|
||||
fab!(:tag) { Fabricate(:tag, name: "cooltag", topic_count: 324) }
|
||||
fab!(:tag2) { Fabricate(:tag, name: "othertag", topic_count: 66) }
|
||||
fab!(:tag) { Fabricate(:tag, name: "cooltag", staff_topic_count: 324, public_topic_count: 324) }
|
||||
fab!(:tag2) { Fabricate(:tag, name: "othertag", staff_topic_count: 66, public_topic_count: 66) }
|
||||
fab!(:topic) { Fabricate(:topic, category: category, tags: [tag]) }
|
||||
fab!(:post) { Fabricate(:post, topic: topic) }
|
||||
let(:uncategorized_category) { Category.find(SiteSetting.uncategorized_category_id) }
|
||||
|
|
Loading…
Reference in New Issue