From 7c092b0fe051a1240cdbf53450f2fe9b0f9190f3 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 20 Jul 2016 16:21:43 -0400 Subject: [PATCH] FEATURE: add filter to show topics that have not been tagged --- .../discourse/components/tag-drop.js.es6 | 14 +++++++ .../discourse/routes/tags-show.js.es6 | 27 +++++++++----- .../templates/components/tag-drop.hbs | 2 + app/controllers/tags_controller.rb | 9 ++++- config/locales/client.en.yml | 3 ++ lib/topic_query.rb | 4 ++ spec/components/topic_query_spec.rb | 37 +++++++++++-------- 7 files changed, 70 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/components/tag-drop.js.es6 b/app/assets/javascripts/discourse/components/tag-drop.js.es6 index df07fcd0c31..75329a5cd05 100644 --- a/app/assets/javascripts/discourse/components/tag-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/tag-drop.js.es6 @@ -45,6 +45,20 @@ export default Ember.Component.extend({ return I18n.t("tagging.selector_all_tags"); }.property('tag'), + @computed('firstCategory', 'secondCategory') + noTagsUrl() { + var url = '/tags'; + if (this.get('currentCategory')) { + url += this.get('currentCategory.url'); + } + return url + '/none'; + }, + + @computed('tag') + noTagsLabel() { + return I18n.t("tagging.selector_no_tags"); + }, + dropdownButtonClass: function() { var result = 'badge-category category-dropdown-button'; if (Em.isNone(this.get('tag'))) { diff --git a/app/assets/javascripts/discourse/routes/tags-show.js.es6 b/app/assets/javascripts/discourse/routes/tags-show.js.es6 index ea8762745b2..a42e7f7e4d1 100644 --- a/app/assets/javascripts/discourse/routes/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/tags-show.js.es6 @@ -11,7 +11,7 @@ export default Discourse.Route.extend({ }, model(params) { - var tag = this.store.createRecord("tag", { id: Handlebars.Utils.escapeExpression(params.tag_id) }), + var tag = (params.tag_id === 'none' ? null : this.store.createRecord("tag", { id: Handlebars.Utils.escapeExpression(params.tag_id) })), f = ''; if (params.category) { @@ -25,8 +25,8 @@ export default Discourse.Route.extend({ if (params.category) { this.set('categorySlug', params.category); } if (params.parent_category) { this.set('parentCategorySlug', params.parent_category); } - if (this.get("currentUser")) { - // If logged in, we should get the tag"s user settings + if (tag && this.get("currentUser")) { + // If logged in, we should get the tag's user settings return this.store.find("tagNotification", tag.get("id")).then(tn => { this.set("tagNotification", tn); return tag; @@ -45,18 +45,19 @@ export default Discourse.Route.extend({ const categorySlug = this.get('categorySlug'); const parentCategorySlug = this.get('parentCategorySlug'); const filter = this.get('navMode'); + const tag_id = (tag ? tag.id : 'none'); if (categorySlug) { var category = Discourse.Category.findBySlug(categorySlug, parentCategorySlug); if (parentCategorySlug) { - params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag.id}/l/${filter}`; + params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag_id}/l/${filter}`; } else { - params.filter = `tags/c/${categorySlug}/${tag.id}/l/${filter}`; + params.filter = `tags/c/${categorySlug}/${tag_id}/l/${filter}`; } this.set('category', category); } else { - params.filter = `tags/${tag.id}/l/${filter}`; + params.filter = `tags/${tag_id}/l/${filter}`; this.set('category', null); } @@ -74,10 +75,18 @@ export default Discourse.Route.extend({ const filterText = I18n.t('filters.' + this.get('navMode').replace('/', '.') + '.title'), controller = this.controllerFor('tags.show'); - if (this.get('category')) { - return I18n.t('tagging.filters.with_category', { filter: filterText, tag: controller.get('model.id'), category: this.get('category.name')}); + if (controller.get('model.id')) { + if (this.get('category')) { + return I18n.t('tagging.filters.with_category', { filter: filterText, tag: controller.get('model.id'), category: this.get('category.name')}); + } else { + return I18n.t('tagging.filters.without_category', { filter: filterText, tag: controller.get('model.id')}); + } } else { - return I18n.t('tagging.filters.without_category', { filter: filterText, tag: controller.get('model.id')}); + if (this.get('category')) { + return I18n.t('tagging.filters.untagged_with_category', { filter: filterText, category: this.get('category.name')}); + } else { + return I18n.t('tagging.filters.untagged_without_category', { filter: filterText}); + } } }, diff --git a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs index 990a0776f8e..5d9a6581923 100644 --- a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs @@ -2,6 +2,7 @@ {{#if tagId}} {{tagId}} {{else}} + {{allTagsLabel}} {{/if}} @@ -9,6 +10,7 @@
+ {{#if renderTags}} {{#each tags as |t|}}
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index a476c204db7..e0b945931d5 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -57,7 +57,7 @@ class TagsController < ::ApplicationController @list.more_topics_url = list_by_tag_path(tag_id: @tag_id, page: page + 1) @rss = "tag" - if @list.topics.size == 0 && !Tag.where(name: @tag_id).exists? + if @list.topics.size == 0 && params[:tag_id] != 'none' && !Tag.where(name: @tag_id).exists? raise Discourse::NotFound else respond_with_list(@list) @@ -203,7 +203,6 @@ class TagsController < ::ApplicationController topic_ids: param_to_integer_list(:topic_ids), exclude_category_ids: params[:exclude_category_ids], category: params[:category], - tags: [params[:tag_id]], order: params[:order], ascending: params[:ascending], min_posts: params[:min_posts], @@ -217,6 +216,12 @@ class TagsController < ::ApplicationController options[:no_subcategories] = true if params[:no_subcategories] == 'true' options[:slow_platform] = true if slow_platform? + if params[:tag_id] == 'none' + options[:no_tags] = true + else + options[:tags] = [params[:tag_id]] + end + options end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3cb67e0c85d..56876ff59e0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2123,6 +2123,7 @@ en: tagging: all_tags: "All Tags" selector_all_tags: "all tags" + selector_no_tags: "no tags" changed: "tags changed:" tags: "Tags" choose_for_topic: "choose optional tags for this topic" @@ -2139,6 +2140,8 @@ en: filters: without_category: "%{filter} %{tag} topics" with_category: "%{filter} %{tag} topics in %{category}" + untagged_without_category: "%{filter} untagged topics" + untagged_with_category: "%{filter} untagged topics in %{category}" notifications: watching: diff --git a/lib/topic_query.rb b/lib/topic_query.rb index ed4aecf8181..49cef6da060 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -20,6 +20,7 @@ class TopicQuery visible category tags + no_tags order ascending no_subcategories @@ -465,6 +466,9 @@ class TopicQuery else result = result.where("tags.name in (?)", @options[:tags]) end + elsif @options[:no_tags] + # the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags")) + result = result.where.not(:id => TopicTag.select(:topic_id).uniq) end end diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index 0b9b19f8547..1c6e63fceff 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -122,25 +122,32 @@ describe TopicQuery do SiteSetting.tagging_enabled = true end - it "returns topics with the tag when filtered to it" do - tagged_topic1 = Fabricate(:topic, {tags: [tag]}) - tagged_topic2 = Fabricate(:topic, {tags: [other_tag]}) - tagged_topic3 = Fabricate(:topic, {tags: [tag, other_tag]}) - no_tags_topic = Fabricate(:topic) + context "no category filter" do + # create some topics before each test: + let!(:tagged_topic1) { Fabricate(:topic, {tags: [tag]}) } + let!(:tagged_topic2) { Fabricate(:topic, {tags: [other_tag]}) } + let!(:tagged_topic3) { Fabricate(:topic, {tags: [tag, other_tag]}) } + let!(:no_tags_topic) { Fabricate(:topic) } - expect(TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) - expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) + it "returns topics with the tag when filtered to it" do + expect(TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) + expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) - two_tag_topic = TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.find { |t| t.id == tagged_topic3.id } - expect(two_tag_topic.tags.size).to eq(2) + two_tag_topic = TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.find { |t| t.id == tagged_topic3.id } + expect(two_tag_topic.tags.size).to eq(2) - # topics with ANY of the given tags: - expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) - expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) + # topics with ANY of the given tags: + expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) + expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) - # TODO: topics with ALL of the given tags: - # expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) - # expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) + # TODO: topics with ALL of the given tags: + # expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) + # expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) + end + + it "can return topics with no tags" do + expect(TopicQuery.new(moderator, no_tags: true).list_latest.topics.map(&:id)).to eq([no_tags_topic.id]) + end end context "and categories too" do