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:
parent
637c311ca7
commit
b2d24f291b
12 changed files with 374 additions and 262 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
57
app/models/concerns/taggable.rb
Normal file
57
app/models/concerns/taggable.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %>"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
class AddCachedTagListToDisplayAds < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :display_ads, :cached_tag_list, :string
|
||||
end
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
208
spec/models/shared_examples/taggable_spec.rb
Normal file
208
spec/models/shared_examples/taggable_spec.rb
Normal 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
|
||||
Loading…
Add table
Reference in a new issue