FEATURE: House Ads

Allows creating ads within Discourse admin at Plugins > House Ads.
Write the ads in html and style them with CSS in themes.
This commit is contained in:
Neil Lalonde 2019-04-18 17:52:59 -04:00
parent 5272995e74
commit 1bd80e1afe
47 changed files with 1350 additions and 192 deletions

View File

@ -0,0 +1,10 @@
module ::AdPlugin
class HouseAdSettingsController < ::ApplicationController
requires_plugin AdPlugin.plugin_name
def update
HouseAdSetting.update(params[:id], params[:value])
render json: success_json
end
end
end

View File

@ -0,0 +1,49 @@
module ::AdPlugin
class HouseAdsController < ::ApplicationController
requires_plugin AdPlugin.plugin_name
def index
render_json_dump(
house_ads: HouseAd.all.map(&:to_hash),
settings: HouseAdSetting.all
)
end
def create
ad = HouseAd.create(house_ad_params)
if ad.valid?
render_json_dump(house_ad: ad.to_hash)
else
render_json_error(ad)
end
end
def update
if ad = HouseAd.find(house_ad_params[:id])
ad.update(house_ad_params)
else
ad = HouseAd.create(house_ad_params.except(:id))
end
if ad.valid?
render_json_dump(house_ad: ad.to_hash)
else
render_json_error(ad)
end
end
def destroy
if ad = HouseAd.find(house_ad_params[:id])
ad.destroy
else
render_json_error(I18n.t('not_found'), status: 404)
end
end
private
def house_ad_params
params.permit(:id, :name, :html)
end
end
end

100
app/models/house_ad.rb Normal file
View File

@ -0,0 +1,100 @@
module ::AdPlugin
class HouseAd
include ActiveModel::Validations
attr_accessor :id, :name, :html
NAME_REGEX = /\A[[:alnum:]\s\.,'!@#$%&\*\-\+\=:]*\z/i
validates :name, presence: true, format: { with: NAME_REGEX }
validates :html, presence: true
validate do
if self.class.all.any? { |ad| ad.id != self.id && ad.name.downcase == self.name.downcase }
errors.add(:name, :taken) # unique name
end
end
def initialize
@name = "New Ad"
@html = "<div class='house-ad'>New Ad</div>"
end
def self.from_hash(h)
ad = self.new
ad.name = h[:name]
ad.html = h[:html]
ad.id = h[:id].to_i if h[:id]
ad
end
def self.create(attrs)
ad = from_hash(attrs)
ad.save
ad
end
def self.alloc_id
DistributedMutex.synchronize('adplugin-house-ad-id') do
max_id = AdPlugin.pstore_get("ad:_id")
max_id = 1 unless max_id
AdPlugin.pstore_set("ad:_id", max_id + 1)
max_id
end
end
def self.find(id)
if r = AdPlugin::pstore_get("ad:#{id}")
from_hash(r)
else
nil
end
end
def self.all
PluginStoreRow.where(plugin_name: AdPlugin.plugin_name)
.where("key LIKE 'ad:%'")
.where("key != 'ad:_id'")
.map do |psr|
from_hash(PluginStore.cast_value(psr.type_name, psr.value))
end.sort_by { |ad| ad.id }
end
def save
if self.valid?
self.id = self.class.alloc_id if self.id.to_i <= 0
AdPlugin::pstore_set("ad:#{id}", to_hash)
self.class.publish_if_ads_enabled
true
else
false
end
end
def update(attrs)
self.name = attrs[:name]
self.html = attrs[:html]
self.save
end
def to_hash
{
id: @id,
name: @name,
html: @html
}
end
def destroy
AdPlugin::pstore_delete("ad:#{id}")
self.class.publish_if_ads_enabled
end
def self.publish_if_ads_enabled
if AdPlugin::HouseAdSetting.all.any? { |_, adsToShow| !adsToShow.blank? }
AdPlugin::HouseAdSetting.publish_settings
end
end
end
end

View File

@ -0,0 +1,62 @@
module ::AdPlugin
class HouseAdSetting
DEFAULTS = {
topic_list_top: '',
topic_above_post_stream: '',
topic_above_suggested: '',
post_bottom: ''
}
def self.all
settings = DEFAULTS.dup
PluginStoreRow.where(plugin_name: AdPlugin.plugin_name)
.where("key LIKE 'ad-setting:%'")
.each do |psr|
settings[psr.key[11..-1].to_sym] = psr.value
end
settings
end
def self.settings_and_ads
settings = AdPlugin::HouseAdSetting.all
ad_names = settings.values.map { |v| v.split('|') }.flatten.uniq
ads = AdPlugin::HouseAd.all.select { |ad| ad_names.include?(ad.name) }
{
settings: settings.merge(after_nth_post: SiteSetting.house_ads_after_nth_post),
creatives: ads.inject({}) { |h, ad| h[ad.name] = ad.html; h }
}
end
def self.update(setting_name, value)
unless DEFAULTS.keys.include?(setting_name.to_sym)
raise Discourse::NotFound
end
ad_names = value&.split('|') || []
if value && ad_names.any? { |v| v !~ HouseAd::NAME_REGEX }
raise Discourse::InvalidParameters
end
unless ad_names.empty?
ad_names = (HouseAd.all.map(&:name) & ad_names)
end
new_value = ad_names.join('|')
if value.nil? || new_value == DEFAULTS[setting_name.to_sym]
AdPlugin::pstore_delete("ad-setting:#{setting_name}")
else
AdPlugin::pstore_set("ad-setting:#{setting_name}", new_value)
end
publish_settings
end
def self.publish_settings
MessageBus.publish('/site/house-creatives', settings_and_ads)
end
end
end

View File

@ -0,0 +1,10 @@
export default {
resource: "admin.adminPlugins",
path: "/plugins",
map() {
this.route("houseAds", { path: "/house-ads" }, function() {
this.route("index", { path: "/" });
this.route("show", { path: "/:ad_id" });
});
}
};

View File

@ -1,8 +1,8 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
@computed('currentUser.groups')
showToGroups: function(groups) {
@computed("currentUser.groups")
showToGroups(groups) {
const currentUser = Discourse.User.current();
if (
@ -17,5 +17,13 @@ export default Ember.Component.extend({
const noAdsGroupNames = this.siteSettings.no_ads_for_groups.split("|");
return !groups.any(group => noAdsGroupNames.includes(group.name));
},
isNthPost(n) {
if (n && n > 0) {
return this.get("postNumber") % n === 0;
} else {
return false;
}
}
});

View File

@ -0,0 +1,84 @@
import computed from "ember-addons/ember-computed-decorators";
const adConfig = Ember.Object.create({
"google-adsense": {
settingPrefix: "adsense" // settings follow naming convention
},
"google-dfp-ad": {
settingPrefix: "dfp" // settings follow naming convention
},
"amazon-product-links": {
settingPrefix: "amazon",
desktop: {
"topic-list-top": "amazon_topic_list_top_src_code",
"post-bottom": "amazon_post_bottom_src_code",
"topic-above-post-stream": "amazon_topic_above_post_stream_src_code",
"topic-above-suggested": "amazon_topic_above_suggested_src_code"
},
mobile: {
"topic-list-top": "amazon_mobile_topic_list_top_src_code",
"post-bottom": "amazon_mobile_post_bottom_src_code",
"topic-above-post-stream":
"amazon_mobile_topic_above_post_stream_src_code",
"topic-above-suggested": "amazon_mobile_topic_above_suggested_src_code"
}
},
"codefund-ad": {
settingPrefix: "codefund",
desktop: {
"topic-list-top": "codefund_top_of_topic_list_enabled",
"post-bottom": "codefund_below_post_enabled",
"topic-above-post-stream": "codefund_above_post_stream_enabled",
"topic-above-suggested": "codefund_above_suggested_enabled"
}
},
"carbonads-ad": {
settingPrefix: "carbonads",
desktop: {
"topic-list-top": "carbonads_topic_list_top_enabled",
"post-bottom": false,
"topic-above-post-stream": "carbonads_above_post_stream_enabled",
"topic-above-suggested": false
}
}
});
export default Ember.Component.extend({
@computed("placement")
adComponents(placement) {
// Check house ads first
const houseAds = this.site.get("house_creatives"),
adsForSlot = houseAds.settings[placement.replace(/-/g, "_")];
if (
Object.keys(houseAds.creatives).length > 0 &&
!Ember.isBlank(adsForSlot)
) {
return ["house-ad"];
}
return Object.keys(adConfig).filter(adNetwork => {
const config = adConfig[adNetwork];
let settingNames = null,
name;
if (this.site.mobileView) {
settingNames = config.mobile || config.desktop;
} else {
settingNames = config.desktop;
}
if (settingNames) {
name = settingNames[placement];
}
if (name === undefined) {
// follows naming convention: prefix_(mobile_)_{placement}_code
name = `${config.settingPrefix}_${
this.site.mobileView ? "mobile_" : ""
}${placement.replace(/-/g, "_")}_code`;
}
return name !== false && !Ember.isBlank(this.siteSettings[name]);
});
}
});

View File

@ -1,4 +1,4 @@
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import computed from "ember-addons/ember-computed-decorators";
const currentUser = Discourse.User.current();
@ -125,7 +125,11 @@ if (
export default AdComponent.extend({
classNames: ["amazon-product-links"],
showAd: Ember.computed.and("showToTrustLevel", "showToGroups"),
showAd: Ember.computed.and(
"showToTrustLevel",
"showToGroups",
"showAfterPost"
),
init() {
let placement = this.get("placement");
@ -169,5 +173,14 @@ export default AdComponent.extend({
trustLevel &&
trustLevel > Discourse.SiteSettings.amazon_through_trust_level
);
},
@computed("postNumber")
showAfterPost(postNumber) {
if (!postNumber) {
return true;
}
return this.isNthPost(parseInt(this.siteSettings.amazon_nth_post_code));
}
});

View File

@ -1,4 +1,4 @@
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import {
default as computed,
observes

View File

@ -1,4 +1,4 @@
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import {
default as computed,
observes
@ -108,17 +108,26 @@ export default AdComponent.extend({
@computed("currentUser.trust_level")
showToTrustLevel(trustLevel) {
return !(
trustLevel &&
trustLevel > Discourse.SiteSettings.codefund_through_trust_level
trustLevel && trustLevel > this.siteSettings.codefund_through_trust_level
);
},
@computed("showToTrustLevel", "showToGroups")
showAd(showToTrustLevel, showToGroups) {
@computed("showToTrustLevel", "showToGroups", "showAfterPost")
showAd(showToTrustLevel, showToGroups, showAfterPost) {
return (
Discourse.SiteSettings.codefund_property_id &&
this.siteSettings.codefund_property_id &&
showToTrustLevel &&
showToGroups
showToGroups &&
showAfterPost
);
},
@computed("postNumber")
showAfterPost(postNumber) {
if (!postNumber) {
return true;
}
return this.isNthPost(parseInt(this.siteSettings.codefund_nth_post));
}
});

View File

@ -1,4 +1,4 @@
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import {
default as computed,
observes
@ -233,17 +233,26 @@ export default AdComponent.extend({
@computed("currentUser.trust_level")
showToTrustLevel(trustLevel) {
return !(
trustLevel &&
trustLevel > Discourse.SiteSettings.adsense_through_trust_level
trustLevel && trustLevel > this.siteSettings.adsense_through_trust_level
);
},
@computed("showToTrustLevel", "showToGroups")
showAd(showToTrustLevel, showToGroups) {
@computed("showToTrustLevel", "showToGroups", "showAfterPost")
showAd(showToTrustLevel, showToGroups, showAfterPost) {
return (
this.siteSettings.adsense_publisher_code &&
showToTrustLevel &&
showToGroups &&
Discourse.SiteSettings.adsense_publisher_code
showAfterPost
);
},
@computed("postNumber")
showAfterPost(postNumber) {
if (!postNumber) {
return true;
}
return this.isNthPost(parseInt(this.siteSettings.adsense_nth_post_code));
}
});

View File

@ -1,4 +1,4 @@
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component";
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import {
default as computed,
observes,
@ -234,23 +234,32 @@ export default AdComponent.extend({
return `width: ${w}px;`.htmlSafe();
},
@computed("showToTrustLevel", "showToGroups")
showAd(showToTrustLevel, showToGroups) {
@computed("showToTrustLevel", "showToGroups", "showAfterPost")
showAd(showToTrustLevel, showToGroups, showAfterPost) {
return (
Discourse.SiteSettings.dfp_publisher_id &&
this.siteSettings.dfp_publisher_id &&
showToTrustLevel &&
showToGroups
showToGroups &&
showAfterPost
);
},
@computed("currentUser.trust_level")
showToTrustLevel(trustLevel) {
return !(
trustLevel &&
trustLevel > Discourse.SiteSettings.dfp_through_trust_level
trustLevel && trustLevel > this.siteSettings.dfp_through_trust_level
);
},
@computed("postNumber")
showAfterPost(postNumber) {
if (!postNumber) {
return true;
}
return this.isNthPost(parseInt(this.siteSettings.dfp_nth_post_code));
},
@observes("refreshOnChange")
refreshAd() {
var slot = ads[this.get("divId")];

View File

@ -0,0 +1,102 @@
import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component";
import {
default as computed,
observes,
on
} from "ember-addons/ember-computed-decorators";
const adIndex = {
topic_list_top: null,
topic_above_post_stream: null,
topic_above_suggested: null,
post_bottom: null
};
export default AdComponent.extend({
classNames: ["house-creative"],
classNameBindings: ["adUnitClass"],
adHtml: "",
@computed("placement", "showAd")
adUnitClass(placement, showAd) {
return showAd ? `house-${placement}` : "";
},
@computed("showToGroups", "showAfterPost")
showAd(showToGroups, showAfterPost) {
return showToGroups && showAfterPost;
},
@computed("postNumber")
showAfterPost(postNumber) {
if (!postNumber) {
return true;
}
return this.isNthPost(
parseInt(this.site.get("house_creatives.settings.after_nth_post"))
);
},
chooseAdHtml() {
const houseAds = this.site.get("house_creatives"),
placement = this.get("placement").replace(/-/g, "_"),
adNames = this.adsNamesForSlot(placement);
if (adNames.length > 0) {
if (!adIndex[placement]) {
adIndex[placement] = 0;
}
let ad = houseAds.creatives[adNames[adIndex[placement]]] || "";
adIndex[placement] = (adIndex[placement] + 1) % adNames.length;
return ad;
} else {
return "";
}
},
adsNamesForSlot(placement) {
const houseAds = this.site.get("house_creatives"),
adsForSlot = houseAds.settings[placement];
if (
Object.keys(houseAds.creatives).length > 0 &&
!Ember.isBlank(adsForSlot)
) {
return adsForSlot.split("|");
} else {
return [];
}
},
@observes("refreshOnChange")
refreshAd() {
if (this.get("listLoading")) {
return;
}
this.set("adHtml", this.chooseAdHtml());
},
didInsertElement() {
this._super(...arguments);
if (!this.get("showAd")) {
return;
}
if (this.get("listLoading")) {
return;
}
if (!adIndex["topic_list_top"]) {
// start at a random spot in the ad inventory
Object.keys(adIndex).forEach(placement => {
const adNames = this.adsNamesForSlot(placement);
adIndex[placement] = Math.floor(Math.random() * adNames.length);
});
}
this.refreshAd();
}
});

View File

@ -0,0 +1,29 @@
import MultiSelectComponent from "select-kit/components/multi-select";
import computed from "ember-addons/ember-computed-decorators";
const { makeArray } = Ember;
export default MultiSelectComponent.extend({
classNames: "house-ads-chooser",
filterable: true,
filterPlaceholder: "admin.adplugin.house_ads.filter_placeholder",
tokenSeparator: "|",
allowCreate: false,
allowAny: false,
settingValue: "",
computeContent() {
return makeArray(this.get("choices"));
},
// called after a selection is made
mutateValues(values) {
this.set("settingValue", values.join(this.get("tokenSeparator")));
},
// called when first rendered
computeValues() {
return this.get("settingValue")
.split(this.get("tokenSeparator"))
.filter(c => c);
}
});

View File

@ -0,0 +1,9 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import computed from "ember-addons/ember-computed-decorators";
import HouseAdsSetting from "discourse/plugins/discourse-adplugin/discourse/components/house-ads-setting";
export default HouseAdsSetting.extend({
classNames: "house-ads-setting house-ads-list-setting",
adNames: Ember.computed.mapBy("allAds", "name")
});

View File

@ -0,0 +1,57 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { i18n, propertyNotEqual } from "discourse/lib/computed";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
classNames: "house-ads-setting",
adValue: "",
saving: false,
savingStatus: "",
title: i18n("name", "admin.adplugin.house_ads.%@.title"),
help: i18n("name", "admin.adplugin.house_ads.%@.description"),
changed: propertyNotEqual("adValue", "value"),
init() {
this._super(...arguments);
this.set("adValue", this.get("value"));
},
actions: {
save() {
if (!this.get("saving")) {
this.setProperties({
saving: true,
savingStatus: I18n.t("saving")
});
ajax(
`/admin/plugins/adplugin/house_ad_settings/${this.get("name")}.json`,
{
type: "PUT",
data: { value: this.get("adValue") }
}
)
.then(data => {
const adSettings = this.get("adSettings");
adSettings.set(this.get("name"), this.get("adValue"));
this.setProperties({
value: this.get("adValue"),
savingStatus: I18n.t("saved")
});
})
.catch(popupAjaxError)
.finally(() => {
this.setProperties({
saving: false,
savingStatus: ""
});
});
}
},
cancel() {
this.set("adValue", this.get("value"));
}
}
});

View File

@ -0,0 +1,5 @@
export default Ember.Controller.extend({
adminPluginsHouseAds: Ember.inject.controller("adminPlugins.houseAds"),
houseAds: Ember.computed.alias("adminPluginsHouseAds.model"),
adSettings: Ember.computed.alias("adminPluginsHouseAds.houseAdsSettings")
});

View File

@ -0,0 +1,93 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyNotEqual } from "discourse/lib/computed";
import { bufferedProperty } from "discourse/mixins/buffered-content";
export default Ember.Controller.extend(bufferedProperty("model"), {
adminPluginsHouseAds: Ember.inject.controller("adminPlugins.houseAds"),
saving: false,
savingStatus: "",
nameDirty: propertyNotEqual("buffered.name", "model.name"),
htmlDirty: propertyNotEqual("buffered.html", "model.html"),
dirty: Ember.computed.or("nameDirty", "htmlDirty"),
disableSave: Ember.computed.not("dirty"),
actions: {
save() {
if (!this.get("saving")) {
this.setProperties({
saving: true,
savingStatus: I18n.t("saving")
});
const data = {},
buffered = this.get("buffered"),
newRecord = !buffered.get("id");
if (!newRecord) {
data.id = buffered.get("id");
}
data.name = buffered.get("name");
data.html = buffered.get("html");
ajax(
newRecord
? `/admin/plugins/adplugin/house_ads`
: `/admin/plugins/adplugin/house_ads/${buffered.get("id")}`,
{
type: newRecord ? "POST" : "PUT",
data
}
)
.then(data => {
this.commitBuffer();
this.set("savingStatus", I18n.t("saved"));
if (newRecord) {
const model = this.get("model");
model.set("id", data.house_ad.id);
const houseAds = this.get("adminPluginsHouseAds.model");
if (!houseAds.includes(model)) {
houseAds.pushObject(model);
}
this.transitionToRoute(
"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.transitionToRoute("adminPlugins.houseAds.index");
return;
}
ajax(`/admin/plugins/adplugin/house_ads/${model.get("id")}`, {
type: "DELETE"
})
.then(() => {
houseAds.removeObject(model);
this.transitionToRoute("adminPlugins.houseAds.index");
})
.catch(() => bootbox.alert(I18n.t("generic_error")));
}
}
});

View File

@ -0,0 +1,3 @@
export default Ember.Controller.extend({
loadingAds: true
});

View File

@ -0,0 +1,7 @@
export default Discourse.Route.extend({
actions: {
moreSettings() {
this.transitionTo("adminSiteSettingsCategory", "ad_plugin");
}
}
});

View File

@ -0,0 +1,15 @@
export default Discourse.Route.extend({
model(params) {
if (params.ad_id === "new") {
return Ember.Object.create({
name: I18n.t("admin.adplugin.house_ads.new_name"),
html: ""
});
} else {
return this.modelFor("adminPlugins.houseAds").findBy(
"id",
parseInt(params.ad_id, 10)
);
}
}
});

View File

@ -0,0 +1,20 @@
import { ajax } from "discourse/lib/ajax";
export default Discourse.Route.extend({
settings: null,
model(params) {
return ajax("/admin/plugins/adplugin/house_ads.json").then(data => {
this.set("settings", Ember.Object.create(data.settings));
return data.house_ads.map(ad => Ember.Object.create(ad));
});
},
setupController(controller, model) {
controller.setProperties({
model,
houseAdsSettings: this.get("settings"),
loadingAds: false
});
}
});

View File

@ -0,0 +1,23 @@
{{#d-section class="house-ads-settings content-body"}}
<div>{{i18n 'admin.adplugin.house_ads.description'}}</div>
{{#unless houseAds.length}}
<p>
{{#link-to 'adminPlugins.houseAds.show' 'new'}}
{{i18n 'admin.adplugin.house_ads.get_started'}}
{{/link-to}}
</p>
{{else}}
<form class="form-horizontal">
{{house-ads-list-setting name="topic_list_top" value=adSettings.topic_list_top allAds=houseAds adSettings=adSettings}}
{{house-ads-list-setting name="topic_above_post_stream" value=adSettings.topic_above_post_stream allAds=houseAds adSettings=adSettings}}
{{house-ads-list-setting name="topic_above_suggested" value=adSettings.topic_above_suggested allAds=houseAds adSettings=adSettings}}
{{house-ads-list-setting name="post_bottom" value=adSettings.post_bottom allAds=houseAds adSettings=adSettings}}
{{d-button label="admin.adplugin.house_ads.more_settings"
icon="cog"
class="btn-default"
action=(route-action "moreSettings")}}
</form>
{{/unless}}
{{/d-section}}

View File

@ -0,0 +1,26 @@
{{#d-section class="edit-house-ad content-body"}}
<h1>{{text-field class="house-ad-name" value=buffered.name}}</h1>
<div class="controls">
{{ace-editor content=buffered.html mode="html"}}
</div>
<div class="controls">
{{d-button
action=(action "save")
disabled=disableSave
class="btn-primary"
label="admin.adplugin.house_ads.save"}}
{{#if saving}}
{{savingStatus}}
{{else}}
{{#if dirty}}
<a href {{action "cancel"}}>{{i18n 'cancel'}}</a>
{{/if}}
{{/if}}
{{d-button
action=(action "destroy")
class="btn-danger delete-button"
label="admin.adplugin.house_ads.delete"}}
</div>
{{/d-section}}

View File

@ -0,0 +1,27 @@
<div class="adplugin-mgmt">
<h1>{{i18n 'admin.adplugin.house_ads.title'}}</h1>
{{#if model.length}}
<div class="content-list">
<div class="house-ads-actions">
{{#link-to 'adminPlugins.houseAds.show' 'new' class="btn btn-primary"}}
{{d-icon "plus"}}
<span>{{i18n 'admin.adplugin.house_ads.new'}}</span>
{{/link-to}}
{{#link-to 'adminPlugins.houseAds.index' class="btn btn-default"}}
{{d-icon "cog"}}
<span>{{i18n 'admin.adplugin.house_ads.settings'}}</span>
{{/link-to}}
</div>
<ul class="house-ads-list">
{{#each model as |ad|}}
<li class="house-ads-list-item">
{{#link-to 'adminPlugins.houseAds.show' ad.id}}
{{ad.name}}
{{/link-to}}
</li>
{{/each}}
</ul>
</div>
{{/if}}
{{outlet}}
</div>

View File

@ -0,0 +1,8 @@
{{#each adComponents as |adComponent|}}
{{component adComponent
placement=placement
refreshOnChange=refreshOnChange
category=category
listLoading=listLoading
postNumber=postNumber}}
{{/each}}

View File

@ -1,43 +0,0 @@
{{#if site.mobileView}}
{{#if model.postSpecificCountAdsense}}
{{#if siteSettings.adsense_mobile_post_bottom_code}}
{{google-adsense placement="post-bottom" postNumber=model.post_number}}
{{/if}}
{{/if}}
{{#if model.postSpecificCountDFP}}
{{#if siteSettings.dfp_mobile_post_bottom_code}}
{{google-dfp-ad placement="post-bottom" category=model.topic.category.slug postNumber=model.post_number}}
{{/if}}
{{/if}}
{{#if model.postSpecificCountAmazon}}
{{#if siteSettings.amazon_mobile_post_bottom_src_code}}
{{amazon-product-links placement="post-bottom" postNumber=model.post_number}}
{{/if}}
{{/if}}
{{#if model.postSpecificCountCodeFund}}
{{#if siteSettings.codefund_below_post_enabled}}
{{codefund-ad placement="post-bottom" postNumber=model.post_number}}
{{/if}}
{{/if}}
{{else}}
{{#if model.postSpecificCountAdsense}}
{{#if siteSettings.adsense_post_bottom_code}}
{{google-adsense placement="post-bottom" postNumber=model.post_number}}
{{/if}}
{{/if}}
{{#if model.postSpecificCountDFP}}
{{#if siteSettings.dfp_post_bottom_code}}
{{google-dfp-ad placement="post-bottom" category=model.topic.category.slug postNumber=model.post_number}}
{{/if}}
{{/if}}
{{#if model.postSpecificCountAmazon}}
{{#if siteSettings.amazon_post_bottom_src_code}}
{{amazon-product-links placement="post-bottom" postNumber=model.post_number}}
{{/if}}
{{/if}}
{{#if model.postSpecificCountCodeFund}}
{{#if siteSettings.codefund_below_post_enabled}}
{{codefund-ad placement="post-bottom" postNumber=model.post_number}}
{{/if}}
{{/if}}
{{/if}}

View File

@ -0,0 +1,3 @@
{{#if showAd}}
{{{adHtml}}}
{{/if}}

View File

@ -0,0 +1,9 @@
<label for="{{name}}">{{title}}</label>
{{house-ads-chooser settingValue=adValue choices=adNames}}
<div class='setting-controls'>
{{#if changed}}
{{d-button class="ok" action=(action "save") icon="check"}}
{{d-button class="cancel" action=(action "cancel") icon="times"}}
{{/if}}
</div>
<p class='help'>{{help}}</p>

View File

@ -0,0 +1,9 @@
<label for="{{name}}">{{title}}</label>
{{text-field value=adValue classNames="house-ads-text-input"}}
<div class='setting-controls'>
{{#if changed}}
{{d-button class="ok" action=(action "save") icon="check"}}
{{d-button class="cancel" action=(action "cancel") icon="times"}}
{{/if}}
</div>
<p class='help'>{{help}}</p>

View File

@ -0,0 +1 @@
{{ad-slot placement="post-bottom" category=model.topic.category.slug postNumber=model.post_number}}

View File

@ -1,33 +1 @@
{{#if site.mobileView}}
{{#if siteSettings.adsense_mobile_topic_list_top_code}}
{{google-adsense placement="topic-list-top" listLoading=listLoading}}
{{/if}}
{{#if siteSettings.dfp_mobile_topic_list_top_code}}
{{google-dfp-ad placement="topic-list-top" refreshOnChange=listLoading category=category.slug listLoading=listLoading}}
{{/if}}
{{#if siteSettings.amazon_mobile_topic_list_top_src_code}}
{{amazon-product-links placement="topic-list-top" listLoading=listLoading}}
{{/if}}
{{#if siteSettings.codefund_top_of_topic_list_enabled}}
{{codefund-ad placement="topic-list-top" listLoading=listLoading refreshOnChange=listLoading}}
{{/if}}
{{#if siteSettings.carbonads_topic_list_top_enabled}}
{{carbonads-ad}}
{{/if}}
{{else}}
{{#if siteSettings.adsense_topic_list_top_code}}
{{google-adsense placement="topic-list-top" listLoading=listLoading}}
{{/if}}
{{#if siteSettings.dfp_topic_list_top_code}}
{{google-dfp-ad placement="topic-list-top" refreshOnChange=listLoading category=category.slug listLoading=listLoading}}
{{/if}}
{{#if siteSettings.amazon_topic_list_top_src_code}}
{{amazon-product-links placement="topic-list-top" listLoading=listLoading}}
{{/if}}
{{#if siteSettings.codefund_top_of_topic_list_enabled}}
{{codefund-ad placement="topic-list-top" listLoading=listLoading refreshOnChange=listLoading}}
{{/if}}
{{#if siteSettings.carbonads_topic_list_top_enabled}}
{{carbonads-ad}}
{{/if}}
{{/if}}
{{ad-slot placement="topic-list-top" refreshOnChange=listLoading category=category.slug listLoading=listLoading}}

View File

@ -1 +1 @@
{{adplugin-container model=this}}
{{post-bottom-ad model=this}}

View File

@ -1,33 +1 @@
{{#if site.mobileView}}
{{#if siteSettings.adsense_mobile_topic_above_post_stream_code}}
{{google-adsense placement="topic-above-post-stream"}}
{{/if}}
{{#if siteSettings.dfp_mobile_topic_above_post_stream_code}}
{{google-dfp-ad placement="topic-above-post-stream" refreshOnChange=model.id category=model.category.slug}}
{{/if}}
{{#if siteSettings.amazon_mobile_topic_above_post_stream_src_code}}
{{amazon-product-links placement="topic-above-post-stream"}}
{{/if}}
{{#if siteSettings.codefund_above_post_stream_enabled}}
{{codefund-ad placement="topic-above-post-stream"}}
{{/if}}
{{#if siteSettings.carbonads_above_post_stream_enabled}}
{{carbonads-ad}}
{{/if}}
{{else}}
{{#if siteSettings.adsense_topic_above_post_stream_code}}
{{google-adsense placement="topic-above-post-stream"}}
{{/if}}
{{#if siteSettings.dfp_topic_above_post_stream_code}}
{{google-dfp-ad placement="topic-above-post-stream" refreshOnChange=model.id category=model.category.slug}}
{{/if}}
{{#if siteSettings.amazon_topic_above_post_stream_src_code}}
{{amazon-product-links placement="topic-above-post-stream"}}
{{/if}}
{{#if siteSettings.codefund_above_post_stream_enabled}}
{{codefund-ad placement="topic-above-post-stream"}}
{{/if}}
{{#if siteSettings.carbonads_above_post_stream_enabled}}
{{carbonads-ad}}
{{/if}}
{{/if}}
{{ad-slot placement="topic-above-post-stream" refreshOnChange=model.id category=model.category.slug}}

View File

@ -1,27 +1 @@
{{#if site.mobileView}}
{{#if siteSettings.adsense_mobile_topic_above_suggested_code}}
{{google-adsense placement="topic-above-suggested"}}
{{/if}}
{{#if siteSettings.dfp_mobile_topic_above_suggested_code}}
{{google-dfp-ad placement="topic-above-suggested" refreshOnChange=model.id category=model.category.slug}}
{{/if}}
{{#if siteSettings.amazon_mobile_topic_above_suggested_src_code}}
{{amazon-product-links placement="topic-above-suggested"}}
{{/if}}
{{#if siteSettings.codefund_above_suggested_enabled}}
{{codefund-ad placement="topic-above-suggested"}}
{{/if}}
{{else}}
{{#if siteSettings.adsense_topic_above_suggested_code}}
{{google-adsense placement="topic-above-suggested"}}
{{/if}}
{{#if siteSettings.dfp_topic_above_suggested_code}}
{{google-dfp-ad placement="topic-above-suggested" refreshOnChange=model.id category=model.category.slug}}
{{/if}}
{{#if siteSettings.amazon_topic_above_suggested_src_code}}
{{amazon-product-links placement="topic-above-suggested"}}
{{/if}}
{{#if siteSettings.codefund_above_suggested_enabled}}
{{codefund-ad placement="topic-above-suggested"}}
{{/if}}
{{/if}}
{{ad-slot placement="topic-above-suggested" refreshOnChange=model.id category=model.category.slug}}

View File

@ -4,39 +4,11 @@ import { withPluginApi } from "discourse/lib/plugin-api";
export default {
name: "initialize-ad-plugin",
initialize(container) {
const siteSettings = container.lookup("site-settings:main");
PostModel.reopen({
postSpecificCountDFP: function() {
return this.isNthPost(parseInt(siteSettings.dfp_nth_post_code));
}.property("post_number"),
postSpecificCountAdsense: function() {
return this.isNthPost(parseInt(siteSettings.adsense_nth_post_code));
}.property("post_number"),
postSpecificCountAmazon: function() {
return this.isNthPost(parseInt(siteSettings.amazon_nth_post_code));
}.property("post_number"),
postSpecificCountCodeFund: function() {
return this.isNthPost(parseInt(siteSettings.codefund_nth_post));
}.property("post_number"),
isNthPost: function(n) {
if (n && n > 0) {
return this.get("post_number") % n === 0;
} else {
return false;
}
}
});
withPluginApi("0.1", api => {
api.decorateWidget("post:after", dec => {
if (dec.canConnectComponent) {
return dec.connect({
component: "adplugin-container",
component: "post-bottom-ad",
context: "model"
});
}
@ -48,5 +20,12 @@ export default {
});
});
});
const messageBus = container.lookup('message-bus:main');
if (!messageBus) { return; }
messageBus.subscribe("/site/house-creatives", function (houseAdsSettings) {
Discourse.Site.currentProp("house_creatives", houseAdsSettings);
});
}
};

View File

@ -7,7 +7,7 @@
clear: both;
}
.google-dfp-ad .dfp-ad-unit {
.google-dfp-ad .dfp-ad-unit {
margin: 0 auto;
}
@ -106,6 +106,15 @@ and (max-width : 775px) {
}
}
.house-creative {
margin: 0 auto;
}
.house-creative.house-post-bottom {
margin: 0 0 10px 52px;
clear: both;
}
.codefund-wrapper {
z-index: 1;
font-family: system, "Helvetica Neue", Helvetica, Arial;
@ -222,3 +231,56 @@ and (max-width : 775px) {
letter-spacing: 1px;
color: $quaternary !important;
}
.adplugin-mgmt {
.house-ads-actions {
.btn {
margin-right: 8px;
}
}
.house-ads-list {
margin-top: 1em;
}
.house-ads-settings {
.form-horizontal {
margin-top: 1em;
}
p.help {
margin: 0;
margin-top: 5px;
color: $primary-medium;
font-size: $font-down-1;
clear: both;
}
.house-ads-chooser, .house-ads-text-input {
float: left;
margin-right: 20px;
}
.setting-controls {
float: left;
}
}
.house-ads-list-setting {
margin-bottom: 1.5em;
}
.content-body {
padding-left: 2%;
.controls {
margin-bottom: 1em;
}
.delete-button {
float: right;
}
.ace-wrapper {
position: relative;
height: 270px;
.ace_editor {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
}
}
}

View File

@ -12,3 +12,28 @@ en:
amazon_plugin: 'Amazon'
codefund_plugin: 'CodeFund'
carbonads_plugin: 'Carbon Ads'
adplugin:
house_ads:
title: "House Ads"
new: "New"
settings: "Settings"
new_name: "New House Ad"
save: "Save"
delete: "Delete"
description: "Define your own ads and where they should be displayed."
get_started: "Start by creating a new ad."
filter_placeholder: "Select ads..."
more_settings: "More Settings"
topic_list_top:
title: "Topic list top ads"
description: "Ads to show at the top of topic list pages."
topic_above_post_stream:
title: "Topic above post stream"
description: "Ads to show above the title of a topic on the topic page."
topic_above_suggested:
title: "Topic above suggested"
description: "Ads to show after the last post in a topic, above the suggested topics."
post_bottom:
title: "Between posts"
description: "Ads to show in between posts, after every N posts."

View File

@ -1,6 +1,7 @@
en:
site_settings:
no_ads_for_groups: "Don't show ads to users in these groups."
house_ads_after_nth_post: 'If "Between posts" house ads are defined, show an ad after every N posts, where N is this value.'
dfp_publisher_id: "Input your Google Ad Manager (formerly called DFP) network code, which is found in your network settings."
dfp_through_trust_level: "Show your ads to users based on trust levels. Users with trust level higher than this value will not see ads."

View File

@ -3,6 +3,11 @@ ad_plugin:
client: true
default: ""
type: group_list
house_ads_after_nth_post:
client: true
default: 20
min: 1
max: 10000
adsense_plugin:
adsense_publisher_code:

View File

@ -6,8 +6,37 @@
register_asset "stylesheets/adplugin.scss"
add_admin_route 'admin.adplugin.house_ads.title', 'houseAds'
module ::AdPlugin
def self.plugin_name
'discourse-adplugin'.freeze
end
def self.pstore_get(key)
PluginStore.get(AdPlugin.plugin_name, key)
end
def self.pstore_set(key, value)
PluginStore.set(AdPlugin.plugin_name, key, value)
end
def self.pstore_delete(key)
PluginStore.remove(AdPlugin.plugin_name, key)
end
end
after_initialize do
require_dependency File.expand_path('../app/models/house_ad', __FILE__)
require_dependency File.expand_path('../app/models/house_ad_setting', __FILE__)
require_dependency File.expand_path('../app/controllers/house_ads_controller', __FILE__)
require_dependency File.expand_path('../app/controllers/house_ad_settings_controller', __FILE__)
require_dependency 'application_controller'
add_to_serializer :site, :house_creatives do
AdPlugin::HouseAdSetting.settings_and_ads
end
class ::AdstxtController < ::ApplicationController
skip_before_action :check_xhr
@ -18,7 +47,19 @@ after_initialize do
end
end
class AdPlugin::Engine < ::Rails::Engine
engine_name 'adplugin'
isolate_namespace AdPlugin
end
AdPlugin::Engine.routes.draw do
root to: 'house_ads#index'
resources :house_ads, only: [:index, :create, :update, :destroy]
resources :house_ad_settings, only: [:update]
end
Discourse::Application.routes.append do
get '/ads.txt' => "adstxt#index"
mount ::AdPlugin::Engine, at: '/admin/plugins/adplugin', constraints: AdminConstraint.new
end
end

View File

@ -0,0 +1,70 @@
require 'rails_helper'
describe AdPlugin::HouseAdSetting do
let(:defaults) { AdPlugin::HouseAdSetting::DEFAULTS }
describe '#all' do
subject { AdPlugin::HouseAdSetting.all }
it "returns defaults when nothing has been set" do
expect(subject).to eq(defaults)
end
it "returns defaults and overrides" do
AdPlugin::pstore_set('ad-setting:topic_list_top', 'Banner')
expect(subject[:topic_list_top]).to eq('Banner')
expect(subject.except(:topic_list_top)).to eq(
defaults.except(:topic_list_top)
)
end
end
describe '#update' do
before do
AdPlugin::HouseAd.create(name: "Banner", html: "<p>Banner</p>")
AdPlugin::HouseAd.create(name: "Donate", html: "<p>Donate</p>")
end
it "can set override for the first time" do
expect {
AdPlugin::HouseAdSetting.update(:topic_list_top, 'Banner|Donate')
}.to change { PluginStoreRow.count }.by(1)
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq('Banner|Donate')
end
it "can update an existing override" do
AdPlugin::pstore_set('ad-setting:topic_list_top', 'Banner')
expect {
AdPlugin::HouseAdSetting.update(:topic_list_top, 'Banner|Donate')
}.to_not change { PluginStoreRow.count }
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq('Banner|Donate')
end
it "removes ad names that don't exist" do
AdPlugin::HouseAdSetting.update(:topic_list_top, 'Coupon|Banner|Donate')
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq('Banner|Donate')
end
it "can reset to default" do
AdPlugin::pstore_set('ad-setting:topic_list_top', 'Banner')
expect {
AdPlugin::HouseAdSetting.update(:topic_list_top, '')
}.to change { PluginStoreRow.count }.by(-1)
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq('')
end
it "raises error on invalid setting name" do
expect {
AdPlugin::HouseAdSetting.update(:nope, 'Click Me')
}.to raise_error(Discourse::NotFound)
expect(AdPlugin::pstore_get('ad-setting:nope')).to be_nil
end
it "raises error on invalid value" do
expect {
AdPlugin::HouseAdSetting.update(:topic_list_top, '<script>')
}.to raise_error(Discourse::InvalidParameters)
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq('')
end
end
end

View File

@ -0,0 +1,160 @@
require 'rails_helper'
describe AdPlugin::HouseAd do
let(:valid_attrs) { {
name: 'Find A Mechanic',
html: '<div class="house-ad find-a-mechanic"><a href="https://mechanics.example.com">Find A Mechanic!</a></div>'
} }
describe '#find' do
let!(:ad) { AdPlugin::HouseAd.create(valid_attrs) }
it "returns nil if no match" do
expect(AdPlugin::HouseAd.find(100)).to be_nil
end
it "can retrieve by id" do
r = AdPlugin::HouseAd.find(ad.id)
expect(r&.name).to eq(valid_attrs[:name])
expect(r&.html).to eq(valid_attrs[:html])
end
end
describe '#all' do
it "returns empty array if no records" do
expect(AdPlugin::HouseAd.all).to eq([])
end
it "returns an array of records" do
AdPlugin::HouseAd.create(valid_attrs)
AdPlugin::HouseAd.create(valid_attrs.merge(name: "Ad 2", html: "<div>Ad 2 Here</div>"))
all = AdPlugin::HouseAd.all
expect(all.size).to eq(2)
expect(all.map(&:name)).to contain_exactly("Ad 2", valid_attrs[:name])
expect(all.map(&:html)).to contain_exactly("<div>Ad 2 Here</div>", valid_attrs[:html])
end
end
describe "save" do
it "assigns an id and attrs for new record" do
ad = AdPlugin::HouseAd.from_hash(valid_attrs)
expect(ad.save).to eq(true)
expect(ad.name).to eq(valid_attrs[:name])
expect(ad.html).to eq(valid_attrs[:html])
expect(ad.id.to_i > 0).to eq(true)
ad2 = AdPlugin::HouseAd.from_hash(valid_attrs.merge(name: 'Find Another Mechanic'))
expect(ad2.save).to eq(true)
expect(ad2.id).to_not eq(ad.id)
end
it "updates existing record" do
ad = AdPlugin::HouseAd.create(valid_attrs)
id = ad.id
ad.name = 'Sell Your Car'
ad.html = '<div class="house-ad">Sell Your Car!</div>'
expect(ad.save).to eq(true)
ad = AdPlugin::HouseAd.find(id)
expect(ad.name).to eq('Sell Your Car')
expect(ad.html).to eq('<div class="house-ad">Sell Your Car!</div>')
expect(ad).to be_valid
end
describe "errors" do
it "blank name" do
ad = AdPlugin::HouseAd.from_hash(valid_attrs.merge(name: ''))
expect(ad.save).to eq(false)
expect(ad).to_not be_valid
expect(ad.errors.full_messages).to be_present
expect(ad.errors[:name]).to be_present
expect(ad.errors.count).to eq(1)
end
it "duplicate name" do
existing = AdPlugin::HouseAd.create(valid_attrs)
ad = AdPlugin::HouseAd.from_hash(valid_attrs)
expect(ad.save).to eq(false)
expect(ad).to_not be_valid
expect(ad.errors.full_messages).to be_present
expect(ad.errors[:name]).to be_present
expect(ad.errors.count).to eq(1)
end
it "duplicate name, different case" do
existing = AdPlugin::HouseAd.create(valid_attrs.merge(name: 'mechanic'))
ad = AdPlugin::HouseAd.create(valid_attrs.merge(name: 'Mechanic'))
expect(ad.save).to eq(false)
expect(ad).to_not be_valid
expect(ad.errors[:name]).to be_present
expect(ad.errors.count).to eq(1)
end
it "blank html" do
ad = AdPlugin::HouseAd.from_hash(valid_attrs.merge(html: ''))
expect(ad.save).to eq(false)
expect(ad).to_not be_valid
expect(ad.errors.full_messages).to be_present
expect(ad.errors[:html]).to be_present
expect(ad.errors.count).to eq(1)
end
it "invalid name" do
ad = AdPlugin::HouseAd.from_hash(valid_attrs.merge(name: '<script>'))
expect(ad.save).to eq(false)
expect(ad).to_not be_valid
expect(ad.errors[:name]).to be_present
expect(ad.errors.count).to eq(1)
end
end
end
describe 'create' do
it "can create new records" do
ad = AdPlugin::HouseAd.create(valid_attrs)
expect(ad).to be_a(AdPlugin::HouseAd)
expect(ad.id).to be_present
expect(ad.name).to eq(valid_attrs[:name])
expect(ad.html).to eq(valid_attrs[:html])
end
it "validates attributes" do
ad = AdPlugin::HouseAd.create({name: '', html: ''})
expect(ad).to be_a(AdPlugin::HouseAd)
expect(ad).to_not be_valid
expect(ad.errors.full_messages).to be_present
expect(ad.errors.count).to eq(2)
end
end
describe 'destroy' do
it "can delete a record" do
ad = AdPlugin::HouseAd.create(valid_attrs)
ad.destroy
expect(AdPlugin::HouseAd.find(ad.id)).to be_nil
end
end
describe 'update' do
let(:ad) { AdPlugin::HouseAd.create(valid_attrs) }
it "updates existing record" do
expect(
ad.update(
name: 'Mechanics 4 Hire',
html: '<a href="https://mechanics.example.com">Find A Mechanic!</a>'
)
).to eq(true)
after_save = AdPlugin::HouseAd.find(ad.id)
expect(after_save.name).to eq('Mechanics 4 Hire')
expect(after_save.html).to eq('<a href="https://mechanics.example.com">Find A Mechanic!</a>')
end
it "validates attributes" do
expect(
ad.update(name: '', html: '')
).to eq(false)
expect(ad).to_not be_valid
expect(ad.errors.full_messages).to be_present
expect(ad.errors.count).to eq(2)
end
end
end

View File

@ -0,0 +1,46 @@
require 'rails_helper'
describe AdPlugin::HouseAdSettingsController do
let(:admin) { Fabricate(:admin) }
before do
AdPlugin::HouseAd.create(name: "Banner", html: "<p>Banner</p>")
end
describe "update" do
let(:valid_params) { { value: 'Banner' } }
it "error if not logged in" do
put '/admin/plugins/adplugin/house_ad_settings/topic_list_top.json', params: valid_params
expect(response.status).to eq(404)
end
it "error if not staff" do
sign_in(Fabricate(:user))
put '/admin/plugins/adplugin/house_ad_settings/topic_list_top.json', params: valid_params
expect(response.status).to eq(404)
end
context "logged in as admin" do
before do
sign_in(admin)
end
it "changes the setting" do
put '/admin/plugins/adplugin/house_ad_settings/topic_list_top.json', params: valid_params
expect(response.status).to eq(200)
expect(AdPlugin::HouseAdSetting.all[:topic_list_top]).to eq(valid_params[:value])
end
it "errors on invalid setting name" do
put '/admin/plugins/adplugin/house_ad_settings/nope-nope.json', params: valid_params
expect(response.status).to eq(404)
end
it "errors on invalid setting value" do
put '/admin/plugins/adplugin/house_ad_settings/topic_list_top.json', params: valid_params.merge(value: "Banner|<script>")
expect(response.status).to eq(400)
end
end
end
end

View File

@ -16,6 +16,18 @@ acceptance("AdSense", {
adsense_mobile_post_bottom_code: "mobile_post_bottom_ad_unit",
adsense_mobile_post_bottom_ad_size: "300*250 - medium rectangle",
adsense_nth_post_code: 6
},
site: {
house_creatives: {
settings: {
topic_list_top: "",
topic_above_post_stream: "",
topic_above_suggested: "",
post_bottom: "",
after_nth_post: 20
},
creatives: {}
}
}
});

View File

@ -16,6 +16,18 @@ acceptance("DFP Ads", {
dfp_mobile_post_bottom_code: "mobile_post_bottom_ad_unit",
dfp_mobile_post_bottom_ad_size: "300*250 - medium rectangle",
dfp_nth_post_code: 6
},
site: {
house_creatives: {
settings: {
topic_list_top: "",
topic_above_post_stream: "",
topic_above_suggested: "",
post_bottom: "",
after_nth_post: 20
},
creatives: {}
}
}
});

View File

@ -0,0 +1,69 @@
import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers";
acceptance("House Ads", {
loggedIn: true,
settings: {
house_ads_after_nth_post: 6
},
site: {
house_creatives: {
settings: {
topic_list_top: "Topic List",
topic_above_post_stream: "Above Post Stream",
topic_above_suggested: "Above Suggested",
post_bottom: "Post",
after_nth_post: 6
},
creatives: {
"Topic List": "<div class='h-topic-list'>TOPIC LIST</div>",
"Above Post Stream":
"<div class='h-above-post-stream'>ABOVE POST STREAM</div>",
"Above Suggested":
"<div class='h-above-suggested'>ABOVE SUGGESTED</div>",
Post: "<div class='h-post'>BELOW POST</div>"
}
}
}
});
test("correct ads show", async assert => {
replaceCurrentUser({ staff: false, trust_level: 1 });
await visit("/t/280"); // 20 posts
assert.equal(
find(".h-above-post-stream").length,
1,
"it should render ad at top of topic"
);
assert.equal(
find(".h-above-suggested").length,
1,
"it should render ad above suggested topics"
);
const ads = find(".h-post");
assert.equal(ads.length, 3, "it should render 3 ads");
assert.equal(
find("#post_6 + .widget-connector").find(".h-post").length,
1,
"ad after 6th post"
);
assert.equal(
find("#post_12 + .widget-connector").find(".h-post").length,
1,
"ad after 12th post"
);
assert.equal(
find("#post_18 + .widget-connector").find(".h-post").length,
1,
"ad after 18th post"
);
await visit("/latest");
assert.equal(
find(".h-topic-list").length,
1,
"it should render ad above topic list"
);
});