FEATURE: Add group and category restrictions to house ads (#205)

# Description

This PR adds the ability to apply **group** and **category** restrictions to a **house ad**.

# What is included
- In order to get the group and category selectors to work within `admin/assets/javascripts/discourse/controllers/admin-plugins-house-ads-show.js` I needed to modernize the file. 
- I dropped the `bufferedProperty` implementation in favor of a vanilla ember approach
- I added `category_ids` and `group_ids` to our house ads model
- I added tests for group / category restrictions
- I added a preview button to display the house ad
- `/site.json` would return a object called `house_creatives` and a list of key value pairs that matched the ad name with the html, like so:
```js
{ AD_KEY: ad.html }
```
I need access to the category ids on the client to conditionally render the house ads so the new format will be: 
```js
{ AD_KEY: { html: ad.html, category_ids: ad.category_ids } }
```

# Screenshots
<img width="658" alt="Screenshot 2024-04-08 at 2 39 22 PM" src="https://github.com/discourse/discourse-adplugin/assets/50783505/b44b386d-65a1-4a2a-a487-d735b13357dd">

# Preview Video

https://github.com/discourse/discourse-adplugin/assets/50783505/6d0d8253-afef-4e15-b6fc-c6f696efd169
This commit is contained in:
Isaac Janzen 2024-04-09 11:54:11 -06:00 committed by GitHub
parent c4227de1b5
commit 554f03f3da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 604 additions and 153 deletions

View File

@ -0,0 +1,7 @@
import CategorySelector from "select-kit/components/category-selector";
export default class HouseAdsCategorySelector extends CategorySelector {
get value() {
return this.selectedCategories.map((c) => c.id);
}
}

View File

@ -0,0 +1,18 @@
import { htmlSafe } from "@ember/template";
import DModal from "discourse/components/d-modal";
import i18n from "discourse-common/helpers/i18n";
const Preview = <template>
<DModal
@closeModal={{@closeModal}}
@title={{i18n "admin.adplugin.house_ads.preview"}}
>
<:body>
<div class="house-ad-preview">
{{htmlSafe @model.html}}
</div>
</:body>
</DModal>
</template>;
export default Preview;

View File

@ -1,115 +1,156 @@
import { tracked } from "@glimmer/tracking";
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import { not, or } from "@ember/object/computed"; import EmberObject, { action } from "@ember/object";
import { inject as service } from "@ember/service"; import { service } from "@ember/service";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
import { observes } from "@ember-decorators/object";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyNotEqual } from "discourse/lib/computed"; import Category from "discourse/models/category";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import I18n from "I18n"; import I18n from "I18n";
import Preview from "../components/modal/preview";
export default Controller.extend(bufferedProperty("model"), { export default class adminPluginsHouseAdsShow extends Controller {
adminPluginsHouseAds: controller("adminPlugins.houseAds"), @service router;
router: service(), @service modal;
saving: false, @controller("adminPlugins.houseAds") houseAdsController;
savingStatus: "",
nameDirty: propertyNotEqual("buffered.name", "model.name"), @tracked selectedCategories = [];
htmlDirty: propertyNotEqual("buffered.html", "model.html"), @tracked selectedGroups = [];
visibleToAnonsDirty: propertyNotEqual( @tracked saving = false;
"buffered.visible_to_anons", @tracked savingStatus = "";
"model.visible_to_anons" @tracked buffered;
),
visibleToLoggedInDirty: propertyNotEqual(
"buffered.visible_to_logged_in_users",
"model.visible_to_logged_in_users"
),
dirty: or(
"nameDirty",
"htmlDirty",
"visibleToLoggedInDirty",
"visibleToAnonsDirty"
),
disableSave: not("dirty"),
actions: { @observes("model")
save() { modelChanged() {
if (!this.get("saving")) { this.buffered = new TrackedObject({ ...this.model });
this.setProperties({ this.selectedCategories = this.model.categories || [];
saving: true, this.selectedGroups = this.model.group_ids || [];
savingStatus: I18n.t("saving"), }
});
const data = {}, get disabledSave() {
buffered = this.get("buffered"), for (const key in this.buffered) {
newRecord = !buffered.get("id"); // we don't want to compare the categories array
if (key !== "categories" && this.buffered[key] !== this.model[key]) {
return false;
}
}
return true;
}
if (!newRecord) { @action
data.id = buffered.get("id"); async save() {
} if (!this.saving) {
data.name = buffered.get("name"); this.saving = true;
data.html = buffered.get("html"); this.savingStatus = I18n.t("saving");
data.visible_to_logged_in_users = buffered.get( const data = {};
"visible_to_logged_in_users" const newRecord = !this.buffered.id;
); if (!newRecord) {
data.visible_to_anons = buffered.get("visible_to_anons"); data.id = this.buffered.id;
}
ajax( data.name = this.buffered.name;
data.html = this.buffered.html;
data.visible_to_logged_in_users =
this.buffered.visible_to_logged_in_users;
data.visible_to_anons = this.buffered.visible_to_anons;
data.category_ids = this.buffered.category_ids;
data.group_ids = this.buffered.group_ids;
try {
const ajaxData = await ajax(
newRecord newRecord
? `/admin/plugins/pluginad/house_creatives` ? `/admin/plugins/pluginad/house_creatives`
: `/admin/plugins/pluginad/house_creatives/${buffered.get("id")}`, : `/admin/plugins/pluginad/house_creatives/${this.buffered.id}`,
{ {
type: newRecord ? "POST" : "PUT", type: newRecord ? "POST" : "PUT",
data, data,
} }
);
this.savingStatus = I18n.t("saved");
const houseAds = this.houseAdsController.model;
if (newRecord) {
this.buffered.id = ajaxData.house_ad.id;
if (!houseAds.includes(this.buffered)) {
houseAds.pushObject(EmberObject.create(this.buffered));
}
this.router.transitionTo(
"adminPlugins.houseAds.show",
this.buffered.id
);
} else {
houseAds
.find((ad) => ad.id === this.buffered.id)
.setProperties(this.buffered);
}
} catch (error) {
popupAjaxError(error);
} finally {
this.set("model", this.buffered);
this.saving = false;
this.savingStatus = "";
}
}
}
@action
setCategoryIds(categoryArray) {
this.selectedCategories = categoryArray;
this.buffered.category_ids = categoryArray.map((c) => c.id);
this.setCategoriesForBuffered();
}
@action
setGroupIds(groupIds) {
this.selectedGroups = groupIds;
this.buffered.group_ids = groupIds.map((id) => id);
}
@action
cancel() {
this.buffered = new TrackedObject({ ...this.model });
this.selectedCategories = this.model.categories || [];
this.selectedGroups = this.model.group_ids || [];
this.setCategoriesForBuffered();
}
@action
async destroy() {
if (!this.buffered.id) {
this.router.transitionTo("adminPlugins.houseAds.index");
return;
}
try {
await ajax(
`/admin/plugins/pluginad/house_creatives/${this.buffered.id}`,
{
type: "DELETE",
}
);
this.houseAdsController.model.removeObject(
this.houseAdsController.model.findBy("id", this.buffered.id)
);
this.router.transitionTo("adminPlugins.houseAds.index");
} catch (error) {
popupAjaxError(error);
}
}
@action
openPreview() {
this.modal.show(Preview, {
model: {
html: this.buffered.html,
},
});
}
setCategoriesForBuffered() {
// we need to fetch the categories because the serializer is not being used
// to attach the category object to the house ads
this.buffered.categories = this.buffered.category_ids
? this.buffered.category_ids.map((categoryId) =>
Category.findById(categoryId)
) )
.then((ajaxData) => { : [];
this.commitBuffer(); }
this.set("savingStatus", I18n.t("saved")); }
if (newRecord) {
const model = this.get("model");
model.set("id", ajaxData.house_ad.id);
const houseAds = this.get("adminPluginsHouseAds.model");
if (!houseAds.includes(model)) {
houseAds.pushObject(model);
}
this.router.transitionTo(
"adminPlugins.houseAds.show",
model.get("id")
);
}
})
.catch(popupAjaxError)
.finally(() => {
this.setProperties({
saving: false,
savingStatus: "",
});
});
}
},
cancel() {
this.rollbackBuffer();
},
destroy() {
const houseAds = this.get("adminPluginsHouseAds.model");
const model = this.get("model");
if (!model.get("id")) {
this.router.transitionTo("adminPlugins.houseAds.index");
return;
}
ajax(`/admin/plugins/pluginad/house_creatives/${model.get("id")}`, {
type: "DELETE",
})
.then(() => {
houseAds.removeObject(model);
this.router.transitionTo("adminPlugins.houseAds.index");
})
.catch(popupAjaxError);
},
},
});

View File

@ -1,20 +1,22 @@
import EmberObject from "@ember/object"; import { TrackedObject } from "@ember-compat/tracked-built-ins";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n"; import I18n from "I18n";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
model(params) { model(params) {
if (params.ad_id === "new") { if (params.ad_id === "new") {
return EmberObject.create({ return new TrackedObject({
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_logged_in_users: true,
visible_to_anons: true, visible_to_anons: true,
}); });
} else { } else {
return this.modelFor("adminPlugins.houseAds").findBy( return new TrackedObject(
"id", this.modelFor("adminPlugins.houseAds").findBy(
parseInt(params.ad_id, 10) "id",
parseInt(params.ad_id, 10)
)
); );
} }
}, },

View File

@ -1,7 +1,7 @@
<section class="edit-house-ad content-body"> <section class="edit-house-ad content-body">
<h1><TextField @value={{buffered.name}} class="house-ad-name" /></h1> <h1><TextField @value={{this.buffered.name}} class="house-ad-name" /></h1>
<div class="controls"> <div class="controls">
<AceEditor @content={{buffered.html}} @mode="html" /> <AceEditor @content={{this.buffered.html}} @mode="html" />
</div> </div>
<div class="controls"> <div class="controls">
<div class="visibility-settings"> <div class="visibility-settings">
@ -22,25 +22,51 @@
/> />
<span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span> <span>{{i18n "admin.adplugin.house_ads.show_to_anons"}}</span>
</div> </div>
<HouseAdsCategorySelector
@categories={{this.site.categories}}
@selectedCategories={{this.selectedCategories}}
@onChange={{this.setCategoryIds}}
@options={{hash allowAny=true}}
class="house-ads-categories"
/>
<div class="description">
{{i18n "admin.adplugin.house_ads.category_chooser_description"}}
</div>
<GroupChooser
@content={{this.site.groups}}
@onChange={{this.setGroupIds}}
@value={{this.selectedGroups}}
class="banner-groups"
/>
<div class="description">
{{i18n "admin.adplugin.house_ads.group_chooser_description"}}
</div>
</div> </div>
<DButton <DButton
@action={{action "save"}} @action={{this.save}}
@disabled={{disableSave}} @disabled={{this.disabledSave}}
@label="admin.adplugin.house_ads.save" @label="admin.adplugin.house_ads.save"
class="btn-primary save-button" class="btn-primary save-button"
/> />
{{#if saving}} {{#if this.saving}}
{{savingStatus}} {{this.savingStatus}}
{{else}} {{else}}
{{#if dirty}} {{#unless this.disabledSave}}
<a href {{action "cancel"}}>{{i18n "cancel"}}</a> <DButton @action={{this.cancel}} @label="cancel" />
{{/if}} {{/unless}}
{{/if}} {{/if}}
<DButton <DButton
@action={{action "destroy"}} @action={{this.openPreview}}
@label="admin.adplugin.house_ads.preview"
/>
<DButton
@action={{this.destroy}}
@label="admin.adplugin.house_ads.delete" @label="admin.adplugin.house_ads.delete"
class="btn-danger delete-button" class="btn-danger delete-button"
/> />

View File

@ -5,11 +5,23 @@ module ::AdPlugin
requires_plugin AdPlugin.plugin_name requires_plugin AdPlugin.plugin_name
def index def index
render_json_dump(house_ads: HouseAd.all.map(&:to_hash), settings: HouseAdSetting.all) render_json_dump(
house_ads:
HouseAd.all.map do |ad|
ad.to_hash.merge!(categories: Category.secured(@guardian).where(id: ad.category_ids))
end,
settings: HouseAdSetting.all,
)
end end
def show def show
render_json_dump(house_ad: HouseAd.find(params[:id])&.to_hash) house_ad_hash = HouseAd.find(params[:id])&.to_hash
if house_ad_hash
house_ad_hash.merge!(
categories: Category.secured(@guardian).where(id: house_ad_hash[:category_ids]),
)
end
render_json_dump(house_ad: house_ad_hash)
end end
def create def create
@ -41,7 +53,15 @@ module ::AdPlugin
@permitted ||= @permitted ||=
begin begin
permitted = permitted =
params.permit(:id, :name, :html, :visible_to_anons, :visible_to_logged_in_users) params.permit(
:id,
:name,
:html,
:visible_to_anons,
:visible_to_logged_in_users,
group_ids: [],
category_ids: [],
)
permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast( permitted[:visible_to_logged_in_users] = ActiveModel::Type::Boolean.new.cast(
permitted[:visible_to_logged_in_users], permitted[:visible_to_logged_in_users],
) )

View File

@ -4,7 +4,13 @@ module ::AdPlugin
class HouseAd class HouseAd
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :id, :name, :html, :visible_to_logged_in_users, :visible_to_anons attr_accessor :id,
:name,
:html,
:visible_to_logged_in_users,
:visible_to_anons,
:category_ids,
:group_ids
NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i
@ -22,6 +28,8 @@ module ::AdPlugin
@html = "<div class='house-ad'>New Ad</div>" @html = "<div class='house-ad'>New Ad</div>"
@visible_to_logged_in_users = true @visible_to_logged_in_users = true
@visible_to_anons = true @visible_to_anons = true
@group_ids = []
@category_ids = []
end end
def self.from_hash(h) def self.from_hash(h)
@ -29,10 +37,12 @@ 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] if h.key?(
ad.visible_to_logged_in_users = h[:visible_to_logged_in_users] :visible_to_logged_in_users,
end )
ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons) ad.visible_to_anons = h[:visible_to_anons] if h.key?(:visible_to_anons)
ad.group_ids = h[:group_ids].map(&:to_i) if h.key?(:group_ids)
ad.category_ids = h[:category_ids].map(&:to_i) if h.key?(:category_ids)
ad ad
end end
@ -72,8 +82,24 @@ module ::AdPlugin
self.all.select(&:visible_to_anons) self.all.select(&:visible_to_anons)
end end
def self.all_for_logged_in_users def self.all_for_logged_in_users(scope)
self.all.select(&:visible_to_logged_in_users) if scope.nil?
# this is for our admin page, so we don't need to filter by group
self.all.select(&:visible_to_logged_in_users)
else
# otherwise, filter by group and visible categories
self.all.select do |ad|
(
ad.group_ids.any? { |group_id| scope.user.groups.pluck(:id).include?(group_id) } ||
ad.group_ids.include?(Group::AUTO_GROUPS[:everyone]) || ad.group_ids.empty?
) && ad.visible_to_logged_in_users &&
(
ad.category_ids.any? do |category_id|
Category.secured(scope).pluck(:id).include?(category_id)
end || true
)
end
end
end end
def save def save
@ -91,10 +117,13 @@ 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] if attrs.key?(
self.visible_to_logged_in_users = attrs[:visible_to_logged_in_users] :visible_to_logged_in_users,
end )
self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons) self.visible_to_anons = attrs[:visible_to_anons] if attrs.key?(:visible_to_anons)
# ensure that group_ids and category_ids can be set to an empty array
self.group_ids = attrs.key?(:group_ids) ? attrs[:group_ids].map(&:to_i) : []
self.category_ids = attrs.key?(:category_ids) ? attrs[:category_ids].map(&:to_i) : []
self.save self.save
end end
@ -105,6 +134,8 @@ module ::AdPlugin
html: @html, html: @html,
visible_to_logged_in_users: @visible_to_logged_in_users, visible_to_logged_in_users: @visible_to_logged_in_users,
visible_to_anons: @visible_to_anons, visible_to_anons: @visible_to_anons,
group_ids: @group_ids,
category_ids: @category_ids,
} }
end end

View File

@ -21,14 +21,14 @@ module ::AdPlugin
settings settings
end end
def self.settings_and_ads(for_anons: true) def self.settings_and_ads(for_anons: true, scope: nil)
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
if for_anons if for_anons
ads = AdPlugin::HouseAd.all_for_anons ads = AdPlugin::HouseAd.all_for_anons
else else
ads = AdPlugin::HouseAd.all_for_logged_in_users ads = AdPlugin::HouseAd.all_for_logged_in_users(scope)
end end
ads = ads.select { |ad| ad_names.include?(ad.name) } ads = ads.select { |ad| ad_names.include?(ad.name) }
@ -41,7 +41,7 @@ module ::AdPlugin
), ),
creatives: creatives:
ads.inject({}) do |h, ad| ads.inject({}) do |h, ad|
h[ad.name] = ad.html h[ad.name] = { html: ad.html, category_ids: ad.category_ids }
h h
end, end,
} }

View File

@ -78,7 +78,14 @@ export default AdComponent.extend({
} }
let ad = houseAds.creatives[adNames[adIndex[placement]]] || ""; let ad = houseAds.creatives[adNames[adIndex[placement]]] || "";
adIndex[placement] = (adIndex[placement] + 1) % adNames.length; adIndex[placement] = (adIndex[placement] + 1) % adNames.length;
return ad; // check if the ad includes the current category, or if no category restrictions are set for the ad
// otherwise don't show it
if (
!ad.category_ids?.length ||
ad.category_ids.includes(this.currentCategoryId)
) {
return ad.html;
}
} else { } else {
return ""; return "";
} }

View File

@ -214,6 +214,9 @@
} }
.adplugin-mgmt { .adplugin-mgmt {
.house-ad-name {
width: 100%;
}
.house-ads-actions { .house-ads-actions {
.btn { .btn {
margin-right: 8px; margin-right: 8px;
@ -249,6 +252,16 @@
padding-left: 2%; padding-left: 2%;
.visibility-settings { .visibility-settings {
margin-bottom: 1em; margin-bottom: 1em;
.description {
color: var(--primary-medium);
font-size: $font-down-1;
}
.category-selector,
.group-chooser {
margin-top: 1em;
}
} }
.controls { .controls {
margin-bottom: 1em; margin-bottom: 1em;
@ -269,3 +282,7 @@
} }
} }
} }
.house-ad-preview {
width: 100%;
}

View File

@ -1,17 +1,17 @@
en: en:
js: js:
adplugin: adplugin:
advertisement_label: 'ADVERTISEMENT' advertisement_label: "ADVERTISEMENT"
admin_js: admin_js:
admin: admin:
site_settings: site_settings:
categories: categories:
ad_plugin: 'Ad Plugin' ad_plugin: "Ad Plugin"
dfp_plugin: 'DFP/Ad Manager' dfp_plugin: "DFP/Ad Manager"
adsense_plugin: 'AdSense' adsense_plugin: "AdSense"
amazon_plugin: 'Amazon' amazon_plugin: "Amazon"
carbonads_plugin: 'Carbon Ads' carbonads_plugin: "Carbon Ads"
adbutler_plugin: 'AdButler' adbutler_plugin: "AdButler"
adplugin: adplugin:
house_ads: house_ads:
title: "House Ads" title: "House Ads"
@ -26,6 +26,9 @@ en:
more_settings: "More Settings" more_settings: "More Settings"
show_to_anons: "Show to anonymous users" show_to_anons: "Show to anonymous users"
show_to_logged_in_users: "Show to logged in users" show_to_logged_in_users: "Show to logged in users"
category_chooser_description: "Choose the categories where this ad should be displayed or leave empty to show the ad everywhere. The `no_ads_for_categories` site setting has priority over this setting."
group_chooser_description: "Choose the groups that can view this ad or leave empty to show the ad to all signed in users."
preview: "Preview"
topic_list_top: topic_list_top:
title: "Topic list top ads" title: "Topic list top ads"

View File

@ -42,7 +42,7 @@ after_initialize do
reloadable_patch { Guardian.prepend ::AdPlugin::GuardianExtensions } reloadable_patch { Guardian.prepend ::AdPlugin::GuardianExtensions }
add_to_serializer :site, :house_creatives do add_to_serializer :site, :house_creatives do
AdPlugin::HouseAdSetting.settings_and_ads(for_anons: scope.anonymous?) AdPlugin::HouseAdSetting.settings_and_ads(for_anons: scope.anonymous?, scope: scope)
end end
add_to_serializer :topic_view, :tags_disable_ads do add_to_serializer :topic_view, :tags_disable_ads do

View File

@ -73,6 +73,8 @@ describe AdPlugin::HouseAdSetting do
html: "<whatever-anon>", html: "<whatever-anon>",
visible_to_anons: true, visible_to_anons: true,
visible_to_logged_in_users: false, visible_to_logged_in_users: false,
group_ids: [],
category_ids: [],
) )
end end
@ -82,6 +84,8 @@ describe AdPlugin::HouseAdSetting do
html: "<whatever-logged-in>", html: "<whatever-logged-in>",
visible_to_anons: false, visible_to_anons: false,
visible_to_logged_in_users: true, visible_to_logged_in_users: true,
group_ids: [],
category_ids: [],
) )
end end
@ -94,11 +98,21 @@ describe AdPlugin::HouseAdSetting do
anon_message = messages.find { |m| m.channel == "/site/house-creatives/anonymous" } anon_message = messages.find { |m| m.channel == "/site/house-creatives/anonymous" }
logged_in_message = messages.find { |m| m.channel == "/site/house-creatives/logged-in" } 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.data[:creatives]).to eq(
"anon-ad" => {
html: "<whatever-anon>",
category_ids: [],
},
)
expect(anon_message.group_ids).to eq(nil) expect(anon_message.group_ids).to eq(nil)
expect(anon_message.user_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.data[:creatives]).to eq(
"logged-in-ad" => {
html: "<whatever-logged-in>",
category_ids: [],
},
)
expect(logged_in_message.group_ids).to eq([Group::AUTO_GROUPS[:trust_level_0]]) expect(logged_in_message.group_ids).to eq([Group::AUTO_GROUPS[:trust_level_0]])
expect(logged_in_message.user_ids).to eq(nil) expect(logged_in_message.user_ids).to eq(nil)
end end

View File

@ -15,6 +15,8 @@ describe AdPlugin::HouseAd do
html: "<div>ANON</div>", html: "<div>ANON</div>",
visible_to_logged_in_users: false, visible_to_logged_in_users: false,
visible_to_anons: true, visible_to_anons: true,
group_ids: [],
category_ids: [],
) )
end end
@ -24,6 +26,8 @@ describe AdPlugin::HouseAd do
html: "<div>LOGGED IN</div>", html: "<div>LOGGED IN</div>",
visible_to_logged_in_users: true, visible_to_logged_in_users: true,
visible_to_anons: false, visible_to_anons: false,
group_ids: [],
category_ids: [],
) )
end end
@ -68,11 +72,12 @@ describe AdPlugin::HouseAd do
describe ".all_for_logged_in_users" do describe ".all_for_logged_in_users" do
let!(:anon_ad) { create_anon_ad } let!(:anon_ad) { create_anon_ad }
let!(:logged_in_ad) { create_logged_in_ad } let!(:logged_in_ad) { create_logged_in_ad }
let!(:user) { Fabricate(:user) }
it "doesn't include ads for anonymous users" do it "doesn't include ads for anonymous users" do
expect(AdPlugin::HouseAd.all_for_logged_in_users.map(&:id)).to contain_exactly( expect(
logged_in_ad.id, AdPlugin::HouseAd.all_for_logged_in_users(Guardian.new(user)).map(&:id),
) ).to contain_exactly(logged_in_ad.id)
end end
end end

View File

@ -2,6 +2,8 @@
describe AdPlugin::HouseAdsController do describe AdPlugin::HouseAdsController do
let(:admin) { Fabricate(:admin) } let(:admin) { Fabricate(:admin) }
let(:category) { Fabricate(:category) }
let(:group) { Fabricate(:group) }
let!(:ad) do let!(:ad) do
AdPlugin::HouseAd.create( AdPlugin::HouseAd.create(
@ -9,6 +11,8 @@ describe AdPlugin::HouseAdsController do
html: "<p>Banner</p>", html: "<p>Banner</p>",
visible_to_anons: true, visible_to_anons: true,
visible_to_logged_in_users: false, visible_to_logged_in_users: false,
category_ids: [],
group_ids: [],
) )
end end
@ -23,6 +27,8 @@ describe AdPlugin::HouseAdsController do
html: ad.html, html: ad.html,
visible_to_anons: "false", visible_to_anons: "false",
visible_to_logged_in_users: "true", visible_to_logged_in_users: "true",
category_ids: [category.id],
group_ids: [group.id],
} }
expect(response.status).to eq(200) expect(response.status).to eq(200)
expect(response.parsed_body["house_ad"].symbolize_keys).to eq( expect(response.parsed_body["house_ad"].symbolize_keys).to eq(
@ -31,6 +37,8 @@ describe AdPlugin::HouseAdsController do
html: ad.html, html: ad.html,
visible_to_anons: false, visible_to_anons: false,
visible_to_logged_in_users: true, visible_to_logged_in_users: true,
category_ids: [category.id],
group_ids: [group.id],
) )
ad_copy = AdPlugin::HouseAd.find(ad.id) ad_copy = AdPlugin::HouseAd.find(ad.id)
@ -38,6 +46,8 @@ describe AdPlugin::HouseAdsController do
expect(ad_copy.html).to eq(ad.html) expect(ad_copy.html).to eq(ad.html)
expect(ad_copy.visible_to_anons).to eq(false) expect(ad_copy.visible_to_anons).to eq(false)
expect(ad_copy.visible_to_logged_in_users).to eq(true) expect(ad_copy.visible_to_logged_in_users).to eq(true)
expect(ad_copy.category_ids).to eq([category.id])
expect(ad_copy.group_ids).to eq([group.id])
end end
end end
@ -51,6 +61,8 @@ describe AdPlugin::HouseAdsController do
html: "blah <h4cked>", html: "blah <h4cked>",
visible_to_anons: "false", visible_to_anons: "false",
visible_to_logged_in_users: "true", visible_to_logged_in_users: "true",
group_ids: [group.id],
category_ids: [category.id],
} }
expect(response.status).to eq(404) expect(response.status).to eq(404)
@ -59,6 +71,8 @@ describe AdPlugin::HouseAdsController do
expect(ad_copy.html).to eq(ad.html) expect(ad_copy.html).to eq(ad.html)
expect(ad_copy.visible_to_anons).to eq(true) expect(ad_copy.visible_to_anons).to eq(true)
expect(ad_copy.visible_to_logged_in_users).to eq(false) expect(ad_copy.visible_to_logged_in_users).to eq(false)
expect(ad_copy.category_ids).to eq([])
expect(ad_copy.group_ids).to eq([])
end end
end end
end end

View File

@ -1,7 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec.describe SiteController do RSpec.describe SiteController do
fab!(:group)
fab!(:private_category) { Fabricate(:private_category, group: group) }
fab!(:user) fab!(:user)
fab!(:group_2) { Fabricate(:group) }
fab!(:user_with_group) { Fabricate(:user, group_ids: [group.id]) }
let!(:anon_ad) do let!(:anon_ad) do
AdPlugin::HouseAd.create( AdPlugin::HouseAd.create(
@ -9,6 +14,8 @@ RSpec.describe SiteController do
html: "<div>ANON</div>", html: "<div>ANON</div>",
visible_to_logged_in_users: false, visible_to_logged_in_users: false,
visible_to_anons: true, visible_to_anons: true,
group_ids: [],
category_ids: [],
) )
end end
@ -18,6 +25,30 @@ RSpec.describe SiteController do
html: "<div>LOGGED IN</div>", html: "<div>LOGGED IN</div>",
visible_to_logged_in_users: true, visible_to_logged_in_users: true,
visible_to_anons: false, visible_to_anons: false,
group_ids: [],
category_ids: [],
)
end
let!(:logged_in_ad_with_category) do
AdPlugin::HouseAd.create(
name: "logged-in-ad-with-category",
html: "<div>LOGGED IN WITH CATEGORY</div>",
visible_to_logged_in_users: true,
visible_to_anons: false,
group_ids: [group.id],
category_ids: [private_category.id],
)
end
let!(:logged_in_ad_with_group_2) do
AdPlugin::HouseAd.create(
name: "logged-in-ad-with-group",
html: "<div>LOGGED IN WITH GROUP</div>",
visible_to_logged_in_users: true,
visible_to_anons: false,
group_ids: [group_2.id],
category_ids: [],
) )
end end
@ -27,19 +58,49 @@ RSpec.describe SiteController do
html: "<div>EVERYONE</div>", html: "<div>EVERYONE</div>",
visible_to_logged_in_users: true, visible_to_logged_in_users: true,
visible_to_anons: true, visible_to_anons: true,
group_ids: [],
category_ids: [],
) )
end end
before { AdPlugin::HouseAdSetting.update("topic_list_top", "logged-in-ad|anon-ad|everyone-ad") } let!(:everyone_group_ad) do
AdPlugin::HouseAd.create(
name: "everyone-group-ad",
html: "<div>EVERYONE</div>",
visible_to_logged_in_users: true,
visible_to_anons: false,
group_ids: [Group::AUTO_GROUPS[:everyone]],
category_ids: [],
)
end
before do
AdPlugin::HouseAdSetting.update(
"topic_list_top",
"logged-in-ad|anon-ad|everyone-ad|logged-in-ad-with-category|logged-in-ad-with-group|everyone-group-ad",
)
end
describe "#site" do describe "#site" do
context "when logged in" do context "when logged in" do
before { sign_in(user) }
it "only includes ads that are visible to logged in users" do it "only includes ads that are visible to logged in users" do
sign_in(user)
get "/site.json"
# excluded logged_in_ad_with_group_2 and logged_in_ad_with_category
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"logged-in-ad",
"everyone-group-ad",
"everyone-ad",
)
end
it "includes ads that are within the logged in user's category permissions" do
sign_in(user_with_group)
get "/site.json" get "/site.json"
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly( expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"logged-in-ad", "logged-in-ad",
"everyone-group-ad",
"logged-in-ad-with-category",
"everyone-ad", "everyone-ad",
) )
end end
@ -48,6 +109,7 @@ RSpec.describe SiteController do
context "when anonymous" do context "when anonymous" do
it "only includes ads that are visible to anonymous users" do it "only includes ads that are visible to anonymous users" do
get "/site.json" get "/site.json"
# excludes everyone_group_ad
expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly( expect(response.parsed_body["house_creatives"]["creatives"].keys).to contain_exactly(
"anon-ad", "anon-ad",
"everyone-ad", "everyone-ad",

View File

@ -25,14 +25,26 @@ acceptance("House Ads", function (needs) {
after_nth_topic: 6, after_nth_topic: 6,
}, },
creatives: { creatives: {
"Topic List Top": "<div class='h-topic-list'>TOPIC LIST TOP</div>", "Topic List Top": {
"Above Post Stream": html: "<div class='h-topic-list'>TOPIC LIST TOP</div>",
"<div class='h-above-post-stream'>ABOVE POST STREAM</div>", category_ids: [],
"Above Suggested": },
"<div class='h-above-suggested'>ABOVE SUGGESTED</div>", "Above Post Stream": {
Post: "<div class='h-post'>BELOW POST</div>", html: "<div class='h-above-post-stream'>ABOVE POST STREAM</div>",
"Between Topic List": category_ids: [],
"<div class='h-between-topic-list'>BETWEEN TOPIC LIST</div>", },
"Above Suggested": {
html: "<div class='h-above-suggested'>ABOVE SUGGESTED</div>",
category_ids: [],
},
Post: {
html: "<div class='h-post'>BELOW POST</div>",
category_ids: [],
},
"Between Topic List": {
html: "<div class='h-between-topic-list'>BETWEEN TOPIC LIST</div>",
category_ids: [],
},
}, },
}, },
}); });
@ -114,3 +126,175 @@ acceptance("House Ads", function (needs) {
); );
}); });
}); });
acceptance(
"House Ads | Category and Group Permissions | Authenticated | Display Ad",
function (needs) {
needs.user();
needs.settings({
no_ads_for_categories: "",
});
needs.site({
house_creatives: {
settings: {
topic_list_top: "Topic List Top",
},
creatives: {
"Topic List Top": {
html: "<div class='h-topic-list'>TOPIC LIST TOP</div>",
// match /c/bug/1
category_ids: [1],
},
},
},
});
test("displays ad to users when current category id is included in ad category_ids", async (assert) => {
updateCurrentUser({
staff: false,
trust_level: 1,
show_to_groups: true,
});
await visit("/c/bug/1");
assert
.dom(".h-topic-list")
.exists(
"ad is displayed above the topic list because the current category id is included in the ad category_ids"
);
});
}
);
acceptance(
"House Ads | Category and Group Permissions | Authenticated | Hide Ad",
function (needs) {
needs.user();
needs.settings({
no_ads_for_categories: "",
});
needs.site({
house_creatives: {
settings: {
topic_list_top: "Topic List Top",
},
creatives: {
"Topic List Top": {
html: "<div class='h-topic-list'>TOPIC LIST TOP</div>",
// restrict ad to a different category than /c/bug/1
category_ids: [2],
},
},
},
});
test("hides ad to users when current category id is not included in ad category_ids", async (assert) => {
updateCurrentUser({
staff: false,
trust_level: 1,
show_to_groups: true,
});
await visit("/c/bug/1");
assert
.dom(".h-topic-list")
.doesNotExist(
"ad is not displayed because the current category id is included in the ad category_ids"
);
});
}
);
acceptance(
"House Ads | Category and Group Permissions | Anonymous | Hide Ad",
function (needs) {
needs.settings({
no_ads_for_categories: "",
});
needs.site({
house_creatives: {
settings: {
topic_list_top: "Topic List Top",
},
creatives: {
"Topic List Top": {
html: "<div class='h-topic-list'>TOPIC LIST TOP</div>",
// restrict ad to a different category than /c/bug/1
category_ids: [2],
},
},
},
});
test("hides ad to anon users when current category id is not included in ad category_ids", async (assert) => {
await visit("/c/bug/1");
assert
.dom(".h-topic-list")
.doesNotExist(
"ad is not displayed because the current category id is included in the ad category_ids"
);
});
}
);
acceptance(
"House Ads | Category and Group Permissions | Anonymous | Hide Ad",
function (needs) {
needs.settings({
no_ads_for_categories: "",
});
needs.site({
house_creatives: {
settings: {
topic_list_top: "Topic List Top",
},
creatives: {
"Topic List Top": {
html: "<div class='h-topic-list'>TOPIC LIST TOP</div>",
// restrict ad to a different category than /c/bug/1
category_ids: [2],
},
},
},
});
test("hides ad to anon users when current category id is not included in ad category_ids", async (assert) => {
await visit("/c/bug/1");
assert
.dom(".h-topic-list")
.doesNotExist(
"ad is not displayed because the current category id is included in the ad category_ids"
);
});
}
);
acceptance(
"House Ads | Category and Group Permissions | Anonymous | Show Ad",
function (needs) {
needs.settings({
no_ads_for_categories: "",
});
needs.site({
house_creatives: {
settings: {
topic_list_top: "Topic List Top",
},
creatives: {
"Topic List Top": {
html: "<div class='h-topic-list'>TOPIC LIST TOP</div>",
// match /c/bug/1
category_ids: [1],
},
},
},
});
test("hides ad to anon users when current category id is not included in ad category_ids", async (assert) => {
await visit("/c/bug/1");
assert
.dom(".h-topic-list")
.exists(
"ad is displayed because the current category id is included in the ad category_ids"
);
});
}
);