From 4bd20358669191de5b1f18d36fa314b83b9233f9 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 7 May 2019 19:52:17 -0400 Subject: [PATCH] FEATURE: house ads can be configured to alternate with other ads Use the new "house ads frequency" site setting to configure how often house ads should show, as a percentage. This only applies in ad placements where other ad networks are configured to appear. --- app/models/house_ad_setting.rb | 5 +- .../discourse/components/ad-slot.js.es6 | 169 ++++++++++++++---- .../templates/components/ad-slot.hbs | 10 +- config/locales/server.en.yml | 1 + config/settings.yml | 5 + 5 files changed, 149 insertions(+), 41 deletions(-) diff --git a/app/models/house_ad_setting.rb b/app/models/house_ad_setting.rb index fac5a02..fe0eb33 100644 --- a/app/models/house_ad_setting.rb +++ b/app/models/house_ad_setting.rb @@ -24,7 +24,10 @@ module ::AdPlugin 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), + settings: settings.merge( + after_nth_post: SiteSetting.house_ads_after_nth_post, + house_ads_frequency: SiteSetting.house_ads_frequency + ), creatives: ads.inject({}) { |h, ad| h[ad.name] = ad.html; h } } end diff --git a/assets/javascripts/discourse/components/ad-slot.js.es6 b/assets/javascripts/discourse/components/ad-slot.js.es6 index 6b6aef1..2cc4534 100644 --- a/assets/javascripts/discourse/components/ad-slot.js.es6 +++ b/assets/javascripts/discourse/components/ad-slot.js.es6 @@ -1,14 +1,24 @@ -import computed from "ember-addons/ember-computed-decorators"; +import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad-component"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; const adConfig = Ember.Object.create({ "google-adsense": { - settingPrefix: "adsense" // settings follow naming convention + settingPrefix: "adsense", // settings follow naming convention + enabledSetting: "adsense_publisher_code", + nthPost: "adsense_nth_post_code" }, "google-dfp-ad": { - settingPrefix: "dfp" // settings follow naming convention + settingPrefix: "dfp", // settings follow naming convention + enabledSetting: "dfp_publisher_id", + nthPost: "dfp_nth_post_code" }, "amazon-product-links": { settingPrefix: "amazon", + enabledSetting: false, + nthPost: "amazon_nth_post_code", desktop: { "topic-list-top": "amazon_topic_list_top_src_code", "post-bottom": "amazon_post_bottom_src_code", @@ -25,6 +35,8 @@ const adConfig = Ember.Object.create({ }, "codefund-ad": { settingPrefix: "codefund", + enabledSetting: "codefund_property_id", + nthPost: "codefund_nth_post", desktop: { "topic-list-top": "codefund_top_of_topic_list_enabled", "post-bottom": "codefund_below_post_enabled", @@ -34,6 +46,7 @@ const adConfig = Ember.Object.create({ }, "carbonads-ad": { settingPrefix: "carbonads", + enabledSetting: "carbonads_serve_id", desktop: { "topic-list-top": "carbonads_topic_list_top_enabled", "post-bottom": false, @@ -43,47 +56,133 @@ const adConfig = Ember.Object.create({ } }); -export default Ember.Component.extend({ - @computed("placement") - adComponents(placement) { - // Check house ads first - const houseAds = this.site.get("house_creatives"); - if (!houseAds || !houseAds.settings) { - return []; +const displayCounts = { + houseAds: 0, + allAds: 0 +}; + +export default AdComponent.extend({ + needsUpdate: false, + + /** + * For a given ad placement and optionally a post number if in between posts, + * list all ad network names that are configured to show there. + */ + @computed("placement", "postNumber") + availableAdTypes(placement, postNumber) { + let types = []; + const houseAds = this.site.get("house_creatives"), + placeUnderscored = placement.replace(/-/g, "_"); + + if (houseAds && houseAds.settings) { + const adsForSlot = houseAds.settings[placeUnderscored]; + + if ( + Object.keys(houseAds.creatives).length > 0 && + !Ember.isBlank(adsForSlot) && + (!postNumber || + this.isNthPost(parseInt(houseAds.settings.after_nth_post, 10))) + ) { + types.push("house-ad"); + } } - const 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 => { + Object.keys(adConfig).forEach(adNetwork => { const config = adConfig[adNetwork]; let settingNames = null, name; - if (this.site.mobileView) { - settingNames = config.mobile || config.desktop; - } else { - settingNames = config.desktop; - } + if ( + config.enabledSetting && + !Ember.isBlank(this.siteSettings[config.enabledSetting]) && + (!postNumber || + !config.nthPost || + this.isNthPost(parseInt(this.siteSettings[config.nthPost], 10))) + ) { + if (this.site.mobileView) { + settingNames = config.mobile || config.desktop; + } else { + settingNames = config.desktop; + } - if (settingNames) { - name = settingNames[placement]; - } + 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`; - } + if (name === undefined) { + // follows naming convention: prefix_(mobile_)_{placement}_code + name = `${config.settingPrefix}_${ + this.site.mobileView ? "mobile_" : "" + }${placeUnderscored}_code`; + } - return name !== false && !Ember.isBlank(this.siteSettings[name]); + if (name !== false && !Ember.isBlank(this.siteSettings[name])) { + types.push(adNetwork); + } + } }); + + return types; + }, + + /** + * When house ads are configured to alternate with other ad networks, we + * need to trigger an update of which ad component is shown after + * navigating between topic lists or topics. + */ + @observes("refreshOnChange") + changed() { + if (this.get("listLoading")) { + return; + } + + // force adComponents to be recomputed + this.notifyPropertyChange("needsUpdate"); + }, + + /** + * Returns a list of the names of ad components that should be rendered + * in the given ad placement. It handles alternating between house ads + * and other ad networks. + */ + @computed("placement", "availableAdTypes", "needsUpdate") + adComponents(placement, availableAdTypes) { + if ( + !availableAdTypes.includes("house-ad") || + availableAdTypes.length === 1 + ) { + // Current behaviour is to allow multiple ads from different networks + // to show in the same place. We could change this to choose one somehow. + return availableAdTypes; + } + + const houseAds = this.site.get("house_creatives"); + let houseAdsSkipped = false; + + if (houseAds.settings.house_ads_frequency === 100) { + // house always wins + return ["house-ad"]; + } else if (houseAds.settings.house_ads_frequency > 0) { + // show house ads the given percent of the time + if ( + displayCounts.allAds === 0 || + (100 * displayCounts.houseAds) / displayCounts.allAds < + houseAds.settings.house_ads_frequency + ) { + displayCounts.houseAds += 1; + displayCounts.allAds += 1; + return ["house-ad"]; + } else { + houseAdsSkipped = true; + } + } + + const networkNames = availableAdTypes.filter(x => x !== "house-ad"); + + if (houseAdsSkipped) { + displayCounts.allAds += networkNames.length; + } + + return networkNames; } }); diff --git a/assets/javascripts/discourse/templates/components/ad-slot.hbs b/assets/javascripts/discourse/templates/components/ad-slot.hbs index 7a49e31..ba4e35d 100644 --- a/assets/javascripts/discourse/templates/components/ad-slot.hbs +++ b/assets/javascripts/discourse/templates/components/ad-slot.hbs @@ -1,8 +1,8 @@ {{#each adComponents as |adComponent|}} {{component adComponent - placement=placement - refreshOnChange=refreshOnChange - category=category - listLoading=listLoading - postNumber=postNumber}} + placement=placement + refreshOnChange=refreshOnChange + category=category + listLoading=listLoading + postNumber=postNumber}} {{/each}} diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0426c06..5fbdbfb 100755 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2,6 +2,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.' + house_ads_frequency: "If other ad networks are configured to show in an ad slot, how often should house ads be shown, as a percentage." 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." diff --git a/config/settings.yml b/config/settings.yml index d3f220d..6409cb0 100755 --- a/config/settings.yml +++ b/config/settings.yml @@ -8,6 +8,11 @@ ad_plugin: default: 20 min: 1 max: 10000 + house_ads_frequency: + client: true + default: 100 + min: 0 + max: 100 adsense_plugin: adsense_publisher_code: