Display Ads: Target by Tag on Posts with matching Tag(s) on the user facing side (#18664)

* feat: Add a pack file that pulls in the MultiSelect Component

* feat: move the tags to its own component

* save tags

* refactor: create a getCSRFToken function in the packs files so that it can be used in the admin

* feat: import the new module in request.js

* feat: remove unnecessary id

* feat: first pass of csrf token test

* chore: update the test

* fix: csrf token

* feat: hide the enw functionality behinda  feature flag

* fix: loading form twice

* refactor: import for csrftoken

* chore: update the description of the function

* feat: use acts_on_taggable to craete a relationship between display_ad and tag

* feat: add a tag field to the display_ad form

* feat: add the selected tags from the multiselect autocomplete component to the input text field that references the tag_list

* feat: add the tag_list to the controller so that we can save it to the db with the display ad parameters

* feat: pull out the tag validation from the article and the display_ads into a concern

* feat: write soem tests for validating the tag on the display_ads model

* feat: add the tag_list as a hidden field

* feat: set the selected tags on edit

* feat: add a js class for the placement area

* feat: use the change in the dropdown to determine whether we show the tags field dropdown

* refactor: rename and brak up fucntions

* feat: show the tags field if the value of the placement area is already set

* fix: move if statement out of the change event

* feat: hide tags field and clear the tag list

* refactor: delete the display ads

* tests: ensure that we test the toggle

* fix:ensure that the tags on the hidden field show up as a string in the input field

* feat: update the jsdoc

* fix: no need to replace space with comma since we showing as string in form

* feat: update the name of the label

* fix: add hidden back

* feat: add cached_tag_list to displya_ads + the index

* feat: udpdate the schema

* refactor: move the scopes into the a module that can be re-used between display_ads and articles

* feat: account for showing display ads with the targeted tags that match the article

* feat: write a spec to show the correct ads based on the article_tags

* refactor: change TagListValidateable to Taggable and move out the not_cached_tagged_with_any scope

* test teh relationship between display ads and tags

* feat: write soem tests for validating the tag on the display_ads model

* chore: change empty to blank

* feat: will show display ads with no tags set if there are no article tags

* feat: add a newline
This commit is contained in:
Ridhwana 2022-11-03 15:51:51 +02:00 committed by GitHub
parent 637c311ca7
commit b2d24f291b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 374 additions and 262 deletions

View file

@ -2,7 +2,7 @@ class Article < ApplicationRecord
include CloudinaryHelper
include ActionView::Helpers
include Reactable
include TagListValidateable
include Taggable
include UserSubscriptionSourceable
include PgSearch::Model
@ -288,47 +288,6 @@ class Article < ApplicationRecord
.tagged_with(tag_name)
}
scope :cached_tagged_with, lambda { |tag|
case tag
when String, Symbol
# In Postgres regexes, the [[:<:]] and [[:>:]] are equivalent to "start of
# word" and "end of word", respectively. They're similar to `\b` in Perl-
# compatible regexes (PCRE), but that matches at either end of a word.
# They're more comparable to how vim's `\<` and `\>` work.
where("cached_tag_list ~ ?", "[[:<:]]#{tag}[[:>:]]")
when Array
tag.reduce(self) { |acc, elem| acc.cached_tagged_with(elem) }
when Tag
cached_tagged_with(tag.name)
else
raise TypeError, "Cannot search tags for: #{tag.inspect}"
end
}
scope :cached_tagged_with_any, lambda { |tags|
case tags
when String, Symbol
cached_tagged_with(tags)
when Array
tags
.map { |tag| cached_tagged_with(tag) }
.reduce { |acc, elem| acc.or(elem) }
when Tag
cached_tagged_with(tags.name)
else
raise TypeError, "Cannot search tags for: #{tags.inspect}"
end
}
# We usually try to avoid using Arel directly like this. However, none of the more
# straight-forward ways of negating the above scope worked:
# 1. A subquery doesn't work because we're not dealing with a simple NOT IN scenario.
# 2. where.not(cached_tagged_with_any(tags).where_values_hash) doesn't work because where_values_hash
# only works for simple conditions and returns an empty hash in this case.
scope :not_cached_tagged_with_any, lambda { |tags|
where(cached_tagged_with_any(tags).arel.constraints.reduce(:or).not)
}
scope :active_help, lambda {
stories = published.cached_tagged_with("help").order(created_at: :desc)

View file

@ -1,11 +0,0 @@
module TagListValidateable
extend ActiveSupport::Concern
# we check tags names aren't too long and don't contain non alphabet characters
def validate_tag_name(tag_list)
tag_list.each do |tag|
new_tag = Tag.new(name: tag)
new_tag.validate_name
new_tag.errors.messages[:name].each { |message| errors.add(:tag, "\"#{tag}\" #{message}") }
end
end
end

View file

@ -0,0 +1,57 @@
module Taggable
extend ActiveSupport::Concern
# rubocop:disable Metrics/BlockLength(RuboCop)
included do
scope :cached_tagged_with, lambda { |tag|
case tag
when String, Symbol
# In Postgres regexes, the [[:<:]] and [[:>:]] are equivalent to "start of
# word" and "end of word", respectively. They're similar to `\b` in Perl-
# compatible regexes (PCRE), but that matches at either end of a word.
# They're more comparable to how vim's `\<` and `\>` work.
where("cached_tag_list ~ ?", "[[:<:]]#{tag}[[:>:]]")
when Array
tag.reduce(self) { |acc, elem| acc.cached_tagged_with(elem) }
when Tag
cached_tagged_with(tag.name)
else
raise TypeError, "Cannot search tags for: #{tag.inspect}"
end
}
scope :cached_tagged_with_any, lambda { |tags|
case tags
when String, Symbol
cached_tagged_with(tags)
when Array
tags
.map { |tag| cached_tagged_with(tag) }
.reduce { |acc, elem| acc.or(elem) }
when Tag
cached_tagged_with(tags.name)
else
raise TypeError, "Cannot search tags for: #{tags.inspect}"
end
}
# We usually try to avoid using Arel directly like this. However, none of the more
# straight-forward ways of negating the above scope worked:
# 1. A subquery doesn't work because we're not dealing with a simple NOT IN scenario.
# 2. where.not(cached_tagged_with_any(tags).where_values_hash) doesn't work because where_values_hash
# only works for simple conditions and returns an empty hash in this case.
scope :not_cached_tagged_with_any, lambda { |tags|
where(cached_tagged_with_any(tags).arel.constraints.reduce(:or).not)
}
end
# rubocop:enable Metrics/BlockLength(RuboCop)
# we check tags names aren't too long and don't contain non alphabet characters
def validate_tag_name(tag_list)
tag_list.each do |tag|
new_tag = Tag.new(name: tag)
new_tag.validate_name
new_tag.errors.messages[:name].each { |message| errors.add(:tag, "\"#{tag}\" #{message}") }
end
end
end

View file

@ -1,5 +1,5 @@
class DisplayAd < ApplicationRecord
include TagListValidateable
include Taggable
acts_as_taggable_on :tags
resourcify
@ -32,9 +32,20 @@ class DisplayAd < ApplicationRecord
search: "%#{term}%"
}
def self.for_display(area, user_signed_in)
def self.for_display(area, user_signed_in, article_tags = [])
relation = approved_and_published.where(placement_area: area).order(success_rate: :desc)
if article_tags.any?
display_ads_with_no_tags = relation.where(cached_tag_list: "")
display_ads_with_targeted_article_tags = relation.cached_tagged_with_any(article_tags)
relation = display_ads_with_no_tags.or(display_ads_with_targeted_article_tags)
end
if article_tags.blank?
relation = relation.where(cached_tag_list: "")
end
relation = if user_signed_in
relation.where(display_to: %w[all logged_in])
else

View file

@ -191,7 +191,7 @@
</article>
<% cache("article-bottom-content-#{@article.id}-#{user_signed_in?}-#{(@organization || @user).latest_article_updated_at}", expires_in: 18.hours) do %>
<% @display_ad = DisplayAd.for_display("post_comments", user_signed_in?) %>
<% @display_ad = DisplayAd.for_display("post_comments", user_signed_in?, @article.decorate.cached_tag_list_array) %>
<% if @display_ad %>
<div class="crayons-card crayons-card--secondary p-4 crayons-sponsorship-article text-styles"
data-display-unit data-id="<%= @display_ad.id %>"

View file

@ -0,0 +1,5 @@
class AddCachedTagListToDisplayAds < ActiveRecord::Migration[7.0]
def change
add_column :display_ads, :cached_tag_list, :string
end
end

View file

@ -0,0 +1,9 @@
class AddIndexToDisplayAdsCachedTagList < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
# From <https://www.postgresql.org/docs/11/pgtrgm.html#id-1.11.7.40.7>
# We need a GIN index on `cached_tag_list` to speed up `LIKE` queries
add_index :display_ads, :cached_tag_list, using: :gin, opclass: :gin_trgm_ops, algorithm: :concurrently
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_10_20_120850) do
ActiveRecord::Schema[7.0].define(version: 2022_10_31_133328) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "pg_stat_statements"
@ -456,6 +456,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_10_20_120850) do
create_table "display_ads", force: :cascade do |t|
t.boolean "approved", default: false
t.text "body_markdown"
t.string "cached_tag_list"
t.integer "clicks_count", default: 0
t.datetime "created_at", precision: nil, null: false
t.integer "display_to", default: 0, null: false
@ -467,6 +468,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_10_20_120850) do
t.boolean "published", default: false
t.float "success_rate", default: 0.0
t.datetime "updated_at", precision: nil, null: false
t.index ["cached_tag_list"], name: "index_display_ads_on_cached_tag_list", opclass: :gin_trgm_ops, using: :gin
end
create_table "email_authorizations", force: :cascade do |t|

View file

@ -21,6 +21,7 @@ FactoryBot.define do
with_collection { nil }
past_published_at { Time.current }
end
co_author_ids { [] }
association :user, factory: :user, strategy: :create
description { Faker::Hipster.paragraph(sentence_count: 1)[0..100] }
@ -29,13 +30,22 @@ FactoryBot.define do
URL.url(ActionController::Base.helpers.asset_path("#{rand(1..40)}.png"))
end
end
experience_level_rating { rand(4..6) }
# The tags property in the markdown is a bit of a hack, and this entire factory needs refactoring.
# In the Tagglable spec we want to extract some common scopes from the article and display ad
# models and test them, hence we want to pass through the tag_list property.
# However, the body_markdown caters for the way that we associate tags for the v1 editor.
# Hence, in this test we default to the transient with_tags being set to true, but if we pass a tag_list through
# then we're making the assumption that it is the v2 editor and we do not want the tags on the body markdown.
# Ideally, we want to create a completely different body_markdown without the frontmatter depending on the version
# of the editor since we pass through different JSON based on the editor.
body_markdown do
<<~HEREDOC
---
title: #{title if with_title}
published: #{published}
tags: #{tags if with_tags}
#{"tags: #{tags}" if with_tags && tag_list.blank?}
date: #{date if with_date}
series: #{with_collection&.slug}
canonical_url: #{canonical_url if with_canonical_url}

View file

@ -12,6 +12,7 @@ RSpec.describe Article, type: :model do
include_examples "#sync_reactions_count", :article
it_behaves_like "UserSubscriptionSourceable"
it_behaves_like "Taggable"
describe "validations" do
it { is_expected.to belong_to(:collection).optional }
@ -987,210 +988,6 @@ RSpec.describe Article, type: :model do
end
end
describe ".cached_tagged_with" do
it "can search for a single tag" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
articles = described_class.cached_tagged_with("includeme")
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme").to_a
end
it "can search for a single tag when given a symbol" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
articles = described_class.cached_tagged_with(:includeme)
expect(articles).to include(included)
expect(articles).not_to include(excluded)
expect(articles.to_a).to eq(described_class.tagged_with("includeme").to_a)
end
it "can search for a single tag when given a Tag object" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
tag = Tag.find_by(name: :includeme)
articles = described_class.cached_tagged_with(tag)
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme").to_a
end
it "can search among multiple tags" do
included = [
create(:article, tags: "omg, wtf"),
create(:article, tags: "omg, lol"),
]
excluded = create(:article, tags: "nope, excluded")
articles = described_class.cached_tagged_with("omg")
expect(articles).to include(*included)
expect(articles).not_to include excluded
expect(articles.to_a).to include(*described_class.tagged_with("omg").to_a)
end
it "can search for multiple tags" do
included = create(:article, tags: "includeme, please, lol")
excluded_partial_match = create(:article, tags: "excluded, please")
excluded_no_match = create(:article, tags: "excluded, omg")
articles = described_class.cached_tagged_with(%w[includeme please])
expect(articles).to include included
expect(articles).not_to include excluded_partial_match
expect(articles).not_to include excluded_no_match
expect(articles.to_a).to eq described_class.tagged_with(%w[includeme please]).to_a
end
it "can search for multiple tags passed as an array of symbols" do
included = create(:article, tags: "includeme, please, lol")
excluded_partial_match = create(:article, tags: "excluded, please")
excluded_no_match = create(:article, tags: "excluded, omg")
articles = described_class.cached_tagged_with(%i[includeme please])
expect(articles).to include(included)
expect(articles).not_to include(excluded_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to eq(described_class.tagged_with(%i[includeme please]).to_a)
end
it "can search for multiple tags passed as an array of Tag objects" do
included = create(:article, tags: "includeme, please, lol")
excluded_partial_match = create(:article, tags: "excluded, please")
excluded_no_match = create(:article, tags: "excluded, omg")
tags = Tag.where(name: %i[includeme please]).to_a
articles = described_class.cached_tagged_with(tags)
expect(articles).to include(included)
expect(articles).not_to include(excluded_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to eq(described_class.tagged_with(%i[includeme please]).to_a)
end
end
describe ".cached_tagged_with_any" do
it "can search for a single tag" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
articles = described_class.cached_tagged_with_any("includeme")
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme", any: true).to_a
end
it "can search for a single tag when given a symbol" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
articles = described_class.cached_tagged_with_any(:includeme)
expect(articles).to include(included)
expect(articles).not_to include(excluded)
expect(articles.to_a).to eq(described_class.tagged_with("includeme", any: true).to_a)
end
it "can search for a single tag when given a Tag object" do
included = create(:article, tags: "includeme")
excluded = create(:article, tags: "lol, nope")
tag = Tag.find_by(name: :includeme)
articles = described_class.cached_tagged_with_any(tag)
expect(articles).to include(included)
expect(articles).not_to include(excluded)
expect(articles.to_a).to eq(described_class.tagged_with("includeme", any: true).to_a)
end
it "can search among multiple tags" do
included = [
create(:article, tags: "omg, wtf"),
create(:article, tags: "omg, lol"),
]
excluded = create(:article, tags: "nope, excluded")
articles = described_class.cached_tagged_with_any("omg")
expected = described_class.tagged_with("omg", any: true).to_a
expect(articles).to include(*included)
expect(articles).not_to include excluded
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags" do
included = create(:article, tags: "includeme, please, lol")
included_partial_match = create(:article, tags: "includeme, omg")
excluded_no_match = create(:article, tags: "excluded, omg")
articles = described_class.cached_tagged_with_any(%w[includeme please])
expected = described_class.tagged_with(%w[includeme please], any: true).to_a
expect(articles).to include included
expect(articles).to include included_partial_match
expect(articles).not_to include excluded_no_match
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags when given an array of symbols" do
included = create(:article, tags: "includeme, please, lol")
included_partial_match = create(:article, tags: "includeme, omg")
excluded_no_match = create(:article, tags: "excluded, omg")
articles = described_class.cached_tagged_with_any(%i[includeme please])
expected = described_class.tagged_with(%i[includeme please], any: true).to_a
expect(articles).to include(included)
expect(articles).to include(included_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags when given an array of Tag objects" do
included = create(:article, tags: "includeme, please, lol")
included_partial_match = create(:article, tags: "includeme, omg")
excluded_no_match = create(:article, tags: "excluded, omg")
tags = Tag.where(name: %i[includeme please]).to_a
articles = described_class.cached_tagged_with_any(tags)
expected = described_class.tagged_with(%i[includeme please], any: true).to_a
expect(articles).to include(included)
expect(articles).to include(included_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to include(*expected)
end
end
describe ".not_cached_tagged_with_any" do
it "can exclude multiple tags when given an array of strings" do
included = create(:article, tags: "includeme")
excluded1 = create(:article, tags: "includeme, lol")
excluded2 = create(:article, tags: "includeme, omg")
articles = described_class
.cached_tagged_with_any("includeme")
.not_cached_tagged_with_any(%w[lol omg])
expect(articles).to include included
expect(articles).not_to include excluded1
expect(articles).not_to include excluded2
end
end
context "when callbacks are triggered before save" do
it "assigns path on save" do
expect(article.path).to eq("/#{article.username}/#{article.slug}")

View file

@ -4,6 +4,8 @@ RSpec.describe DisplayAd, type: :model do
let(:organization) { create(:organization) }
let(:display_ad) { create(:display_ad, organization_id: organization.id) }
it_behaves_like "Taggable"
describe "validations" do
describe "builtin validations" do
subject { display_ad }
@ -13,6 +15,7 @@ RSpec.describe DisplayAd, type: :model do
it { is_expected.to validate_presence_of(:placement_area) }
it { is_expected.to validate_presence_of(:body_markdown) }
it { is_expected.to have_many(:tags) }
end
it "allows sidebar_right" do
@ -82,6 +85,68 @@ RSpec.describe DisplayAd, type: :model do
end
end
context "when considering article_tags" do
it "will show the display ads that contain tags that match any of the article tags" do
display_ad = create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "linux, git, go")
create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "career")
article_tags = %w[linux productivity]
expect(described_class.for_display("post_comments", false, article_tags)).to eq(display_ad)
end
it "will show display ads that have no tags set" do
display_ad = create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "")
create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "career")
article_tags = %w[productivity java]
expect(described_class.for_display("post_comments", false, article_tags)).to eq(display_ad)
end
it "will show no display ads if the available display ads have no tags set or do not contain matching tags" do
create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "productivity")
article_tags = %w[javascript]
expect(described_class.for_display("post_comments", false, article_tags)).to be_nil
end
it "will show display ads with no tags set if there are no article tags" do
create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "productivity")
display_ad_without_tags = create(:display_ad, organization_id: organization.id,
placement_area: "post_comments",
published: true,
approved: true,
cached_tag_list: "")
expect(described_class.for_display("post_comments", false)).to eq(display_ad_without_tags)
end
end
context "when display_to is set to 'logged_in' or 'logged_out'" do
let!(:display_ad2) do
create(:display_ad, organization_id: organization.id, published: true, approved: true, display_to: "logged_in")

View file

@ -0,0 +1,208 @@
RSpec.shared_examples "Taggable" do
let(:model) { described_class } # the class that includes the concern
object = described_class.name.underscore.to_sym
describe ".cached_tagged_with" do
it "can search for a single tag" do
included = create(object, tag_list: "includeme")
excluded = create(object, tag_list: "lol, nope")
articles = described_class.cached_tagged_with("includeme")
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme").to_a
end
it "can search for a single tag when given a symbol" do
included = create(object, tag_list: "includeme")
excluded = create(object, tag_list: "lol, nope")
articles = described_class.cached_tagged_with(:includeme)
expect(articles).to include(included)
expect(articles).not_to include(excluded)
expect(articles.to_a).to eq(described_class.tagged_with("includeme").to_a)
end
it "can search for a single tag when given a Tag object" do
included = create(object, tag_list: "includeme")
excluded = create(object, tag_list: "lol, nope")
tag = Tag.find_by(name: :includeme)
articles = described_class.cached_tagged_with(tag)
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme").to_a
end
it "can search among multiple tags" do
included = [
create(object, tag_list: "omg, wtf"),
create(object, tag_list: "omg, lol"),
]
excluded = create(object, tag_list: "nope, excluded")
articles = described_class.cached_tagged_with("omg")
expect(articles).to include(*included)
expect(articles).not_to include excluded
expect(articles.to_a).to include(*described_class.tagged_with("omg").to_a)
end
it "can search for multiple tags" do
included = create(object, tag_list: "includeme, please, lol")
excluded_partial_match = create(object, tag_list: "excluded, please")
excluded_no_match = create(object, tag_list: "excluded, omg")
articles = described_class.cached_tagged_with(%w[includeme please])
expect(articles).to include included
expect(articles).not_to include excluded_partial_match
expect(articles).not_to include excluded_no_match
expect(articles.to_a).to eq described_class.tagged_with(%w[includeme please]).to_a
end
it "can search for multiple tags passed as an array of symbols" do
included = create(object, tag_list: "includeme, please, lol")
excluded_partial_match = create(object, tag_list: "excluded, please")
excluded_no_match = create(object, tag_list: "excluded, omg")
articles = described_class.cached_tagged_with(%i[includeme please])
expect(articles).to include(included)
expect(articles).not_to include(excluded_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to eq(described_class.tagged_with(%i[includeme please]).to_a)
end
it "can search for multiple tags passed as an array of Tag objects" do
included = create(object, tag_list: "includeme, please, lol")
excluded_partial_match = create(object, tag_list: "excluded, please")
excluded_no_match = create(object, tag_list: "excluded, omg")
tags = Tag.where(name: %i[includeme please]).to_a
articles = described_class.cached_tagged_with(tags)
expect(articles).to include(included)
expect(articles).not_to include(excluded_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to eq(described_class.tagged_with(%i[includeme please]).to_a)
end
end
describe ".cached_tagged_with_any" do
it "can search for a single tag" do
included = create(object, tag_list: "includeme")
excluded = create(object, tag_list: "lol, nope")
articles = described_class.cached_tagged_with_any("includeme")
expect(articles).to include included
expect(articles).not_to include excluded
expect(articles.to_a).to eq described_class.tagged_with("includeme", any: true).to_a
end
it "can search for a single tag when given a symbol" do
included = create(object, tag_list: "includeme")
excluded = create(object, tag_list: "lol, nope")
articles = described_class.cached_tagged_with_any(:includeme)
expect(articles).to include(included)
expect(articles).not_to include(excluded)
expect(articles.to_a).to eq(described_class.tagged_with("includeme", any: true).to_a)
end
it "can search for a single tag when given a Tag object" do
included = create(object, tag_list: "includeme")
excluded = create(object, tag_list: "lol, nope")
tag = Tag.find_by(name: :includeme)
articles = described_class.cached_tagged_with_any(tag)
expect(articles).to include(included)
expect(articles).not_to include(excluded)
expect(articles.to_a).to eq(described_class.tagged_with("includeme", any: true).to_a)
end
it "can search among multiple tags" do
included = [
create(object, tag_list: "omg, wtf"),
create(object, tag_list: "omg, lol"),
]
excluded = create(object, tag_list: "nope, excluded")
articles = described_class.cached_tagged_with_any("omg")
expected = described_class.tagged_with("omg", any: true).to_a
expect(articles).to include(*included)
expect(articles).not_to include excluded
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags" do
included = create(object, tag_list: "includeme, please, lol")
included_partial_match = create(object, tag_list: "includeme, omg")
excluded_no_match = create(object, tag_list: "excluded, omg")
articles = described_class.cached_tagged_with_any(%w[includeme please])
expected = described_class.tagged_with(%w[includeme please], any: true).to_a
expect(articles).to include included
expect(articles).to include included_partial_match
expect(articles).not_to include excluded_no_match
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags when given an array of symbols" do
included = create(object, tag_list: "includeme, please, lol")
included_partial_match = create(object, tag_list: "includeme, omg")
excluded_no_match = create(object, tag_list: "excluded, omg")
articles = described_class.cached_tagged_with_any(%i[includeme please])
expected = described_class.tagged_with(%i[includeme please], any: true).to_a
expect(articles).to include(included)
expect(articles).to include(included_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to include(*expected)
end
it "can search for multiple tags when given an array of Tag objects" do
included = create(object, tag_list: "includeme, please, lol")
included_partial_match = create(object, tag_list: "includeme, omg")
excluded_no_match = create(object, tag_list: "excluded, omg")
tags = Tag.where(name: %i[includeme please]).to_a
articles = described_class.cached_tagged_with_any(tags)
expected = described_class.tagged_with(%i[includeme please], any: true).to_a
expect(articles).to include(included)
expect(articles).to include(included_partial_match)
expect(articles).not_to include(excluded_no_match)
expect(articles.to_a).to include(*expected)
end
end
describe ".not_cached_tagged_with_any" do
it "can exclude multiple tags when given an array of strings" do
included = create(object, tag_list: "includeme")
excluded1 = create(object, tag_list: "includeme, lol")
excluded2 = create(object, tag_list: "includeme, omg")
articles = described_class
.cached_tagged_with_any("includeme")
.not_cached_tagged_with_any(%w[lol omg])
expect(articles).to include included
expect(articles).not_to include excluded1
expect(articles).not_to include excluded2
end
end
end