FEATURE: Add per-ad visibility settings for anons and logged-in users (#168)

This commit adds 2 new settings to house ads to control whether an ad is shown to anonymous users and logged in users. Existing ads that were created before this feature will default to true for both settings; i.e., they will remain to be visible to both anonymous and logged-in users, but it will be possible to change the settings. Turning off both settings will effectively disable the ad completely.
This commit is contained in:
Osama Sayegh 2023-04-07 17:56:22 +03:00 committed by GitHub
parent bfd4438b97
commit c2057a5056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 375 additions and 18 deletions

View File

@ -38,7 +38,18 @@ module ::AdPlugin
private private
def house_ad_params def house_ad_params
params.permit(:id, :name, :html) @permitted ||=
begin
permitted =
params.permit(:id, :name, :html, :visible_to_anons, :visible_to_logged_in_users)
permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast(
permitted[:visible_to_logged_in_users],
)
permitted[:visible_to_anons] = ActiveModel::Type::Boolean.new.cast(
permitted[:visible_to_anons],
)
permitted
end
end end
end end
end end

View File

@ -4,7 +4,7 @@ module ::AdPlugin
class HouseAd class HouseAd
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :id, :name, :html attr_accessor :id, :name, :html, :visible_to_logged_in_users, :visible_to_anons
NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i
@ -20,6 +20,8 @@ module ::AdPlugin
def initialize def initialize
@name = "New Ad" @name = "New Ad"
@html = "<div class='house-ad'>New Ad</div>" @html = "<div class='house-ad'>New Ad</div>"
@visible_to_logged_in_users = true
@visible_to_anons = true
end end
def self.from_hash(h) def self.from_hash(h)
@ -27,6 +29,10 @@ module ::AdPlugin
ad.name = h[:name] ad.name = h[:name]
ad.html = h[:html] ad.html = h[:html]
ad.id = h[:id].to_i if h[:id] ad.id = h[:id].to_i if h[:id]
if h.key?(:visible_to_logged_in_users)
ad.visible_to_logged_in_users = h[:visible_to_logged_in_users]
end
ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons)
ad ad
end end
@ -62,10 +68,19 @@ module ::AdPlugin
.sort_by { |ad| ad.id } .sort_by { |ad| ad.id }
end end
def self.all_for_anons
self.all.select(&:visible_to_anons)
end
def self.all_for_logged_in_users
self.all.select(&:visible_to_logged_in_users)
end
def save def save
if self.valid? if self.valid?
self.id = self.class.alloc_id if self.id.to_i <= 0 self.id = self.class.alloc_id if self.id.to_i <= 0
AdPlugin.pstore_set("ad:#{id}", to_hash) AdPlugin.pstore_set("ad:#{id}", to_hash)
Site.clear_anon_cache!
self.class.publish_if_ads_enabled self.class.publish_if_ads_enabled
true true
else else
@ -76,15 +91,26 @@ module ::AdPlugin
def update(attrs) def update(attrs)
self.name = attrs[:name] self.name = attrs[:name]
self.html = attrs[:html] self.html = attrs[:html]
if attrs.key?(:visible_to_logged_in_users)
self.visible_to_logged_in_users = attrs[:visible_to_logged_in_users]
end
self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons)
self.save self.save
end end
def to_hash def to_hash
{ id: @id, name: @name, html: @html } {
id: @id,
name: @name,
html: @html,
visible_to_logged_in_users: @visible_to_logged_in_users,
visible_to_anons: @visible_to_anons,
}
end end
def destroy def destroy
AdPlugin.pstore_delete("ad:#{id}") AdPlugin.pstore_delete("ad:#{id}")
Site.clear_anon_cache!
self.class.publish_if_ads_enabled self.class.publish_if_ads_enabled
end end

View File

@ -21,10 +21,17 @@ module ::AdPlugin
settings settings
end end
def self.settings_and_ads def self.settings_and_ads(for_anons: true)
settings = AdPlugin::HouseAdSetting.all settings = AdPlugin::HouseAdSetting.all
ad_names = settings.values.map { |v| v.split("|") }.flatten.uniq ad_names = settings.values.map { |v| v.split("|") }.flatten.uniq
ads = AdPlugin::HouseAd.all.select { |ad| ad_names.include?(ad.name) }
if for_anons
ads = AdPlugin::HouseAd.all_for_anons
else
ads = AdPlugin::HouseAd.all_for_logged_in_users
end
ads = ads.select { |ad| ad_names.include?(ad.name) }
{ {
settings: settings:
settings.merge( settings.merge(
@ -56,12 +63,18 @@ module ::AdPlugin
else else
AdPlugin.pstore_set("ad-setting:#{setting_name}", new_value) AdPlugin.pstore_set("ad-setting:#{setting_name}", new_value)
end end
Site.clear_anon_cache!
publish_settings publish_settings
end end
def self.publish_settings def self.publish_settings
MessageBus.publish("/site/house-creatives", settings_and_ads) MessageBus.publish("/site/house-creatives/anonymous", settings_and_ads(for_anons: true))
MessageBus.publish(
"/site/house-creatives/logged-in",
settings_and_ads(for_anons: false),
group_ids: [Group::AUTO_GROUPS[:trust_level_0]],
)
end end
end end
end end

View File

@ -14,7 +14,20 @@ export default Controller.extend(bufferedProperty("model"), {
nameDirty: propertyNotEqual("buffered.name", "model.name"), nameDirty: propertyNotEqual("buffered.name", "model.name"),
htmlDirty: propertyNotEqual("buffered.html", "model.html"), htmlDirty: propertyNotEqual("buffered.html", "model.html"),
dirty: or("nameDirty", "htmlDirty"), visibleToAnonsDirty: propertyNotEqual(
"buffered.visible_to_anons",
"model.visible_to_anons"
),
visibleToLoggedInDirty: propertyNotEqual(
"buffered.visible_to_logged_in_users",
"model.visible_to_logged_in_users"
),
dirty: or(
"nameDirty",
"htmlDirty",
"visibleToLoggedInDirty",
"visibleToAnonsDirty"
),
disableSave: not("dirty"), disableSave: not("dirty"),
actions: { actions: {
@ -34,6 +47,10 @@ export default Controller.extend(bufferedProperty("model"), {
} }
data.name = buffered.get("name"); data.name = buffered.get("name");
data.html = buffered.get("html"); data.html = buffered.get("html");
data.visible_to_logged_in_users = buffered.get(
"visible_to_logged_in_users"
);
data.visible_to_anons = buffered.get("visible_to_anons");
ajax( ajax(
newRecord newRecord

View File

@ -8,6 +8,8 @@ export default DiscourseRoute.extend({
return EmberObject.create({ return EmberObject.create({
name: I18n.t("admin.adplugin.house_ads.new_name"), name: I18n.t("admin.adplugin.house_ads.new_name"),
html: "", html: "",
visible_to_logged_in_users: true,
visible_to_anons: true,
}); });
} else { } else {
return this.modelFor("adminPlugins.houseAds").findBy( return this.modelFor("adminPlugins.houseAds").findBy(

View File

@ -4,10 +4,30 @@
{{ace-editor content=buffered.html mode="html"}} {{ace-editor content=buffered.html mode="html"}}
</div> </div>
<div class="controls"> <div class="controls">
<div class="visibility-settings">
<div>
<Input
class="visible-to-logged-in-checkbox"
@type="checkbox"
@checked={{this.buffered.visible_to_logged_in_users}}
/>
<span>{{i18n "admin.adplugin.house_ads.show_to_logged_in_users"}}</span>
</div>
<div>
<Input
class="visible-to-anonymous-checkbox"
@type="checkbox"
@checked={{this.buffered.visible_to_anons}}
/>
<span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span>
</div>
</div>
{{d-button {{d-button
action=(action "save") action=(action "save")
disabled=disableSave disabled=disableSave
class="btn-primary" class="btn-primary save-button"
label="admin.adplugin.house_ads.save" label="admin.adplugin.house_ads.save"
}} }}

View File

@ -28,7 +28,14 @@ export default {
return; return;
} }
messageBus.subscribe("/site/house-creatives", function (houseAdsSettings) { const currentUser = container.lookup("service:current-user");
let channel;
if (currentUser) {
channel = "/site/house-creatives/logged-in";
} else {
channel = "/site/house-creatives/anonymous";
}
messageBus.subscribe(channel, function (houseAdsSettings) {
Site.currentProp("house_creatives", houseAdsSettings); Site.currentProp("house_creatives", houseAdsSettings);
}); });
}, },

View File

@ -241,6 +241,9 @@
} }
.content-body { .content-body {
padding-left: 2%; padding-left: 2%;
.visibility-settings {
margin-bottom: 1em;
}
.controls { .controls {
margin-bottom: 1em; margin-bottom: 1em;
} }

View File

@ -24,6 +24,8 @@ en:
get_started: "Start by creating a new ad." get_started: "Start by creating a new ad."
filter_placeholder: "Select ads..." filter_placeholder: "Select ads..."
more_settings: "More Settings" more_settings: "More Settings"
show_to_anons: "Show to anonymous users"
show_to_logged_in_users: "Show to logged in users"
topic_list_top: topic_list_top:
title: "Topic list top ads" title: "Topic list top ads"

View File

@ -39,7 +39,7 @@ after_initialize do
require_dependency "application_controller" require_dependency "application_controller"
add_to_serializer :site, :house_creatives do add_to_serializer :site, :house_creatives do
AdPlugin::HouseAdSetting.settings_and_ads AdPlugin::HouseAdSetting.settings_and_ads(for_anons: scope.anonymous?)
end end
add_to_serializer :topic_view, :tags_disable_ads do add_to_serializer :topic_view, :tags_disable_ads do

View File

@ -5,7 +5,7 @@ require "rails_helper"
describe AdPlugin::HouseAdSetting do describe AdPlugin::HouseAdSetting do
let(:defaults) { AdPlugin::HouseAdSetting::DEFAULTS } let(:defaults) { AdPlugin::HouseAdSetting::DEFAULTS }
describe "#all" do describe ".all" do
subject { AdPlugin::HouseAdSetting.all } subject { AdPlugin::HouseAdSetting.all }
it "returns defaults when nothing has been set" do it "returns defaults when nothing has been set" do
@ -19,7 +19,7 @@ describe AdPlugin::HouseAdSetting do
end end
end end
describe "#update" do describe ".update" do
before do before do
AdPlugin::HouseAd.create(name: "Banner", html: "<p>Banner</p>") AdPlugin::HouseAd.create(name: "Banner", html: "<p>Banner</p>")
AdPlugin::HouseAd.create(name: "Donate", html: "<p>Donate</p>") AdPlugin::HouseAd.create(name: "Donate", html: "<p>Donate</p>")
@ -67,4 +67,42 @@ describe AdPlugin::HouseAdSetting do
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq("") expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq("")
end end
end end
describe ".publish_settings" do
let!(:anon_ad) do
AdPlugin::HouseAd.create(
name: "anon-ad",
html: "<whatever-anon>",
visible_to_anons: true,
visible_to_logged_in_users: false,
)
end
let!(:logged_in_ad) do
AdPlugin::HouseAd.create(
name: "logged-in-ad",
html: "<whatever-logged-in>",
visible_to_anons: false,
visible_to_logged_in_users: true,
)
end
before { AdPlugin::HouseAdSetting.update("topic_list_top", "logged-in-ad|anon-ad") }
it "publishes different payloads to different channels for anons and logged in users" do
messages = MessageBus.track_publish { AdPlugin::HouseAdSetting.publish_settings }
expect(messages.size).to eq(2)
anon_message = messages.find { |m| m.channel == "/site/house-creatives/anonymous" }
logged_in_message = messages.find { |m| m.channel == "/site/house-creatives/logged-in" }
expect(anon_message.data[:creatives]).to eq("anon-ad" => "<whatever-anon>")
expect(anon_message.group_ids).to eq(nil)
expect(anon_message.user_ids).to eq(nil)
expect(logged_in_message.data[:creatives]).to eq("logged-in-ad" => "<whatever-logged-in>")
expect(logged_in_message.group_ids).to eq([Group::AUTO_GROUPS[:trust_level_0]])
expect(logged_in_message.user_ids).to eq(nil)
end
end
end end

View File

@ -11,7 +11,25 @@ describe AdPlugin::HouseAd do
} }
end end
describe "#find" do def create_anon_ad
AdPlugin::HouseAd.create(
name: "anon-ad",
html: "<div>ANON</div>",
visible_to_logged_in_users: false,
visible_to_anons: true,
)
end
def create_logged_in_ad
AdPlugin::HouseAd.create(
name: "logged-in-ad",
html: "<div>LOGGED IN</div>",
visible_to_logged_in_users: true,
visible_to_anons: false,
)
end
describe ".find" do
let!(:ad) { AdPlugin::HouseAd.create(valid_attrs) } let!(:ad) { AdPlugin::HouseAd.create(valid_attrs) }
it "returns nil if no match" do it "returns nil if no match" do
@ -25,7 +43,7 @@ describe AdPlugin::HouseAd do
end end
end end
describe "#all" do describe ".all" do
it "returns empty array if no records" do it "returns empty array if no records" do
expect(AdPlugin::HouseAd.all).to eq([]) expect(AdPlugin::HouseAd.all).to eq([])
end end
@ -40,7 +58,27 @@ describe AdPlugin::HouseAd do
end end
end end
describe "save" do describe ".all_for_anons" do
let!(:anon_ad) { create_anon_ad }
let!(:logged_in_ad) { create_logged_in_ad }
it "doesn't include ads for logged in users" do
expect(AdPlugin::HouseAd.all_for_anons.map(&:id)).to contain_exactly(anon_ad.id)
end
end
describe ".all_for_logged_in_users" do
let!(:anon_ad) { create_anon_ad }
let!(:logged_in_ad) { create_logged_in_ad }
it "doesn't include ads for anonymous users" do
expect(AdPlugin::HouseAd.all_for_logged_in_users.map(&:id)).to contain_exactly(
logged_in_ad.id,
)
end
end
describe "#save" do
it "assigns an id and attrs for new record" do it "assigns an id and attrs for new record" do
ad = AdPlugin::HouseAd.from_hash(valid_attrs) ad = AdPlugin::HouseAd.from_hash(valid_attrs)
expect(ad.save).to eq(true) expect(ad.save).to eq(true)
@ -112,7 +150,7 @@ describe AdPlugin::HouseAd do
end end
end end
describe "create" do describe ".create" do
it "can create new records" do it "can create new records" do
ad = AdPlugin::HouseAd.create(valid_attrs) ad = AdPlugin::HouseAd.create(valid_attrs)
expect(ad).to be_a(AdPlugin::HouseAd) expect(ad).to be_a(AdPlugin::HouseAd)
@ -130,7 +168,7 @@ describe AdPlugin::HouseAd do
end end
end end
describe "destroy" do describe "#destroy" do
it "can delete a record" do it "can delete a record" do
ad = AdPlugin::HouseAd.create(valid_attrs) ad = AdPlugin::HouseAd.create(valid_attrs)
ad.destroy ad.destroy
@ -138,7 +176,7 @@ describe AdPlugin::HouseAd do
end end
end end
describe "update" do describe "#update" do
let(:ad) { AdPlugin::HouseAd.create(valid_attrs) } let(:ad) { AdPlugin::HouseAd.create(valid_attrs) }
it "updates existing record" do it "updates existing record" do

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
require "rails_helper"
describe AdPlugin::HouseAdsController do
let(:admin) { Fabricate(:admin) }
let!(:ad) do
AdPlugin::HouseAd.create(
name: "Banner",
html: "<p>Banner</p>",
visible_to_anons: true,
visible_to_logged_in_users: false,
)
end
describe "#update" do
context "when used by admins" do
before { sign_in(admin) }
it "updates an existing ad" do
put "/admin/plugins/pluginad/house_creatives/#{ad.id}.json",
params: {
name: ad.name,
html: ad.html,
visible_to_anons: "false",
visible_to_logged_in_users: "true",
}
expect(response.status).to eq(200)
expect(response.parsed_body["house_ad"].symbolize_keys).to eq(
id: ad.id,
name: ad.name,
html: ad.html,
visible_to_anons: false,
visible_to_logged_in_users: true,
)
ad_copy = AdPlugin::HouseAd.find(ad.id)
expect(ad_copy.name).to eq(ad.name)
expect(ad_copy.html).to eq(ad.html)
expect(ad_copy.visible_to_anons).to eq(false)
expect(ad_copy.visible_to_logged_in_users).to eq(true)
end
end
context "when used by non-admins" do
before { sign_in(Fabricate(:user)) }
it "can't update ads" do
put "/admin/plugins/pluginad/house_creatives/#{ad.id}.json",
params: {
name: "non sense goes here",
html: "blah <h4cked>",
visible_to_anons: "false",
visible_to_logged_in_users: "true",
}
expect(response.status).to eq(404)
ad_copy = AdPlugin::HouseAd.find(ad.id)
expect(ad_copy.name).to eq(ad.name)
expect(ad_copy.html).to eq(ad.html)
expect(ad_copy.visible_to_anons).to eq(true)
expect(ad_copy.visible_to_logged_in_users).to eq(false)
end
end
end
end

View File

@ -0,0 +1,76 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe SiteController do
fab!(:user) { Fabricate(:user) }
let!(:anon_ad) do
AdPlugin::HouseAd.create(
name: "anon-ad",
html: "<div>ANON</div>",
visible_to_logged_in_users: false,
visible_to_anons: true,
)
end
let!(:logged_in_ad) do
AdPlugin::HouseAd.create(
name: "logged-in-ad",
html: "<div>LOGGED IN</div>",
visible_to_logged_in_users: true,
visible_to_anons: false,
)
end
let!(:everyone_ad) do
AdPlugin::HouseAd.create(
name: "everyone-ad",
html: "<div>EVERYONE</div>",
visible_to_logged_in_users: true,
visible_to_anons: true,
)
end
before { AdPlugin::HouseAdSetting.update("topic_list_top", "logged-in-ad|anon-ad|everyone-ad") }
describe "#site" do
context "when logged in" do
before { sign_in(user) }
it "only includes ads that are visible to logged in users" do
get "/site.json"
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"logged-in-ad",
"everyone-ad",
)
end
end
context "when anonymous" do
it "only includes ads that are visible to anonymous users" do
get "/site.json"
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"anon-ad",
"everyone-ad",
)
end
it "invalidates cache when an ad is updated" do
get "/site.json"
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"anon-ad",
"everyone-ad",
)
anon_ad.visible_to_anons = false
anon_ad.save
get "/site.json"
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"everyone-ad",
)
end
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
describe "Admin House Ad", type: :system, js: true do
fab!(:admin) { Fabricate(:admin) }
let(:house_ad) do
AdPlugin::HouseAd.create(
name: "some-name",
html: "<div>somecode</div>",
visible_to_anons: true,
visible_to_logged_in_users: false,
)
end
before { sign_in(admin) }
describe "when visiting the page for creating new ads" do
it "should have the visibility checkboxes on by default" do
visit("/admin/plugins/pluginad/house_creatives/new")
expect(find("input.visible-to-anonymous-checkbox").checked?).to eq(true)
expect(find("input.visible-to-logged-in-checkbox").checked?).to eq(true)
end
end
describe "when visiting the page of an existing ad" do
it "the controls reflect the correct state of the ad" do
visit("/admin/plugins/pluginad/house_creatives/#{house_ad.id}")
expect(find("input.house-ad-name").value).to eq(house_ad.name)
expect(find("input.visible-to-anonymous-checkbox").checked?).to eq(true)
expect(find("input.visible-to-logged-in-checkbox").checked?).to eq(false)
# would be nice to assert for the HTML content in ace-editor, but there
# doesn't seem to be a way to check the content in ace-editor from the
# DOM
end
end
end