FEATURE: Adds an API to exclude a tag from a TopicQuery

FEATURE: Adds an API to exclude a tag from a TopicQuery

To exclude a tag from a topic list, add the exclude_tag query parameter. For example: latest?exclude_tag=music

diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index 57ffb9a..992a3c7 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -60,7 +60,8 @@ class TopicQuery
          tags
          match_all_tags
          no_subcategories
-         no_tags)
+         no_tags
+         exclude_tag)
   end
 
   def self.valid_options
@@ -689,6 +690,10 @@ class TopicQuery
         # the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags"))
         result = result.where.not(id: TopicTag.distinct.pluck(:topic_id))
       end
+
+      if @options[:exclude_tag] && tag = Tag.find_by(name: @options[:exclude_tag])
+        result = result.where.not(id: TopicTag.distinct.where(tag_id: tag.id).pluck(:topic_id))
+      end
     end
 
     result = apply_ordering(result, options)
diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb
index a7e7f2e..ec21641 100644
--- a/spec/components/topic_query_spec.rb
+++ b/spec/components/topic_query_spec.rb
@@ -383,6 +383,11 @@ describe TopicQuery do
       fab!(:no_tags_topic) { Fabricate(:topic) }
       let(:synonym) { Fabricate(:tag, target_tag: tag, name: 'synonym') }
 
+      it "excludes a tag if desired" do
+        topics = TopicQuery.new(moderator, exclude_tag: tag.name).list_latest.topics
+        expect(topics.any? { |t| t.tags.include?(tag) }).to eq(false)
+      end
+
       it "returns topics with the tag when filtered to it" do
         expect(TopicQuery.new(moderator, tags: tag.name).list_latest.topics)
           .to contain_exactly(tagged_topic1, tagged_topic3)

GitHub sha: ae13839f98b1b2530a4727a09feee89d7a6ebd88

This commit appears in #14540 which was approved by markvanlan. It was merged by eviltrout.

@eviltrout is this change being used in core? If not I strongly believe we should extend TopicQuery here via a plugin API.

It’s for a customer theme, not a plugin.

After talking through various options I decided that it’s a useful API to allow others to use.

Is it possible for a theme to call Ruby code?

No, not unless there is an API. Themes are JS/CSS bundles.

Ooo icic. So the theme is calling an endpoint with a param that is eventually passed to the TopicQuery class here? If that is the case, I think we should document the param for the API route so that we make it clear why the new option exists in core.

It looks like we only need Tag#id so we can just pluck the id instead of creating the active record object.

@tgxworld there is no place to currently document this in the code base. However you’ll see the PR explicitly adds this to the public API options:

True, but it would be a very small optimization since it’s only querying one record that has very few fields. Also this has already been merged. If you want to make a follow up PR I’d support it but otherwise I consider it low priority.

This commit has been mentioned on Discourse Meta. There might be relevant details there: