From 4fd7caffd6f1cc1101849bfdbb70f02e5af209e6 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 16 Apr 2019 16:57:16 -0400 Subject: [PATCH] FEATURE: use groups to control who sees ads A new setting has been added in Admin > Settings > Ad Plugin called "no ads for groups". Add group names to this list. If a user belongs to any of the groups, they will not see any ads. This is an alternative to using trust level settings like "adsense through trust level". --- .../discourse/components/ad_component.js.es6 | 23 ++++++ .../components/google-adsense.js.es6 | 31 ++++---- .../discourse/components/google-dfp-ad.js.es6 | 73 +++++++++++-------- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 2 + config/settings.yml | 8 ++ .../acceptance/adsense-test.js.es6 | 16 ++++ test/javascripts/acceptance/dfp-test.js.es6 | 72 ++++++++++++++++++ 8 files changed, 183 insertions(+), 43 deletions(-) create mode 100644 assets/javascripts/discourse/components/ad_component.js.es6 create mode 100644 test/javascripts/acceptance/dfp-test.js.es6 diff --git a/assets/javascripts/discourse/components/ad_component.js.es6 b/assets/javascripts/discourse/components/ad_component.js.es6 new file mode 100644 index 0000000..b8d6f30 --- /dev/null +++ b/assets/javascripts/discourse/components/ad_component.js.es6 @@ -0,0 +1,23 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + @computed() + showToGroups: function() { + const currentUser = Discourse.User.current(); + + if ( + !currentUser || + !currentUser.get("groups") || + !this.siteSettings.no_ads_for_groups || + this.siteSettings.no_ads_for_groups.length === 0 + ) { + return true; + } + + const noAdsGroupNames = this.siteSettings.no_ads_for_groups.split("|"); + + return !currentUser + .get("groups") + .any(group => noAdsGroupNames.includes(group.name)); + } +}); diff --git a/assets/javascripts/discourse/components/google-adsense.js.es6 b/assets/javascripts/discourse/components/google-adsense.js.es6 index bdab286..b3a910b 100644 --- a/assets/javascripts/discourse/components/google-adsense.js.es6 +++ b/assets/javascripts/discourse/components/google-adsense.js.es6 @@ -1,3 +1,4 @@ +import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component"; import { default as computed, observes @@ -146,7 +147,7 @@ if (Discourse.SiteSettings.adsense_publisher_code) { } } -export default Ember.Component.extend({ +export default AdComponent.extend({ classNameBindings: [ ":google-adsense", "classForSlot", @@ -193,7 +194,7 @@ export default Ember.Component.extend({ }, @observes("listLoading") - waitForLoad: function() { + waitForLoad() { if (this.get("adRequested")) { return; } // already requested that this ad unit be populated @@ -203,34 +204,34 @@ export default Ember.Component.extend({ }, @computed("ad_width") - isResponsive: function(adWidth) { + isResponsive(adWidth) { return adWidth === "auto"; }, - @computed("placement", "checkTrustLevels") - classForSlot: function(placement, shown) { - return shown ? `adsense-${placement}`.htmlSafe() : ""; + @computed("placement", "showAd") + classForSlot(placement, showAd) { + return showAd ? `adsense-${placement}`.htmlSafe() : ""; }, @computed("isResponsive") - autoAdFormat: function(isResponsive) { + autoAdFormat(isResponsive) { return isResponsive ? "auto".htmlSafe() : false; }, @computed("ad_width", "ad_height", "isResponsive") - adWrapperStyle: function(w, h, isResponsive) { + adWrapperStyle(w, h, isResponsive) { return (isResponsive ? "" : `width: ${w}; height: ${h};`).htmlSafe(); }, @computed("adWrapperStyle", "isResponsive") - adInsStyle: function(adWrapperStyle, isResponsive) { + adInsStyle(adWrapperStyle, isResponsive) { return `display: ${ isResponsive ? "block" : "inline-block" }; ${adWrapperStyle}`.htmlSafe(); }, @computed() - checkTrustLevels: function() { + showToTrustLevel() { return !( currentUser && currentUser.get("trust_level") > @@ -238,8 +239,12 @@ export default Ember.Component.extend({ ); }, - @computed("checkTrustLevels") - showAd: function(shown) { - return shown && Discourse.SiteSettings.adsense_publisher_code; + @computed("showToTrustLevel", "showToGroups") + showAd(showToTrustLevel, showToGroups) { + return ( + showToTrustLevel && + showToGroups && + Discourse.SiteSettings.adsense_publisher_code + ); } }); diff --git a/assets/javascripts/discourse/components/google-dfp-ad.js.es6 b/assets/javascripts/discourse/components/google-dfp-ad.js.es6 index 2c236e6..55a570d 100755 --- a/assets/javascripts/discourse/components/google-dfp-ad.js.es6 +++ b/assets/javascripts/discourse/components/google-dfp-ad.js.es6 @@ -1,3 +1,9 @@ +import AdComponent from "discourse/plugins/discourse-adplugin/discourse/components/ad_component"; +import { + default as computed, + observes, + on +} from "ember-addons/ember-computed-decorators"; import loadScript from "discourse/lib/load-script"; var currentUser = Discourse.User.current(), @@ -198,51 +204,56 @@ function loadGoogle() { return _promise; } -export default Ember.Component.extend({ +export default AdComponent.extend({ classNameBindings: ["adUnitClass"], classNames: ["google-dfp-ad"], loadedGoogletag: false, refreshOnChange: null, - divId: function() { - if (this.get("postNumber")) { - return ( - "div-gpt-ad-" + this.get("placement") + "-" + this.get("postNumber") - ); + @computed("placement", "postNumber") + divId(placement, postNumber) { + if (postNumber) { + return `div-gpt-ad-${placement}-${postNumber}`; } else { - return "div-gpt-ad-" + this.get("placement"); + return `div-gpt-ad-${placement}`; } - }.property("placement", "postNumber"), + }, - adUnitClass: function() { - return "dfp-ad-" + this.get("placement"); - }.property("placement"), + @computed("placement", "showAd") + adUnitClass(placement, showAd) { + return showAd ? `dfp-ad-${placement}` : ""; + }, - adWrapperStyle: function() { - return `width: ${this.get("width")}px; height: ${this.get( - "height" - )}px;`.htmlSafe(); - }.property("width", "height"), + @computed("width", "height") + adWrapperStyle(w, h) { + return `width: ${w}px; height: ${h}px;`.htmlSafe(); + }, - adTitleStyleMobile: function() { - return `width: ${this.get("width")}px;`.htmlSafe(); - }.property("width"), + @computed("width") + adTitleStyleMobile(w) { + return `width: ${w}px;`.htmlSafe(); + }, - showAd: function() { + @computed("showToTrustLevel", "showToGroups") + showAd(showToTrustLevel, showToGroups) { return ( - Discourse.SiteSettings.dfp_publisher_id && this.get("checkTrustLevels") + Discourse.SiteSettings.dfp_publisher_id && + showToTrustLevel && + showToGroups ); - }.property("checkTrustLevels"), + }, - checkTrustLevels: function() { + @computed() + showToTrustLevel() { return !( currentUser && currentUser.get("trust_level") > Discourse.SiteSettings.dfp_through_trust_level ); - }.property("trust_level"), + }, - refreshAd: function() { + @observes("refreshOnChange") + refreshAd() { var slot = ads[this.get("divId")]; if (!(slot && slot.ad)) { return; @@ -260,9 +271,10 @@ export default Ember.Component.extend({ window.googletag.pubads().refresh([ad]); }); } - }.observes("refreshOnChange"), + }, - _initGoogleDFP: function() { + @on("didInsertElement") + _initGoogleDFP() { if (!this.get("showAd")) { return; } @@ -287,7 +299,7 @@ export default Ember.Component.extend({ } }); }); - }.on("didInsertElement"), + }, willRender() { this._super(...arguments); @@ -300,7 +312,8 @@ export default Ember.Component.extend({ this.set("height", size.height); }, - cleanup: function() { + @on("willDestroyElement") + cleanup() { destroySlot(this.get("divId")); - }.on("willDestroyElement") + } }); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6cdb0a9..e45b924 100755 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -6,6 +6,7 @@ en: admin: site_settings: categories: + ad_plugin: 'Ad Plugin' dfp_plugin: 'DFP/Ad Manager' adsense_plugin: 'AdSense' amazon_plugin: 'Amazon' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 934bed9..cfde9d4 100755 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1,5 +1,7 @@ en: site_settings: + no_ads_for_groups: "Don't show ads to users in these groups." + 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 927e277..c5213a2 100755 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,3 +1,11 @@ +ad_plugin: + no_ads_for_groups: + client: true + default: "" + type: list + choices: "Group.pluck(:name)" + list_type: compact + adsense_plugin: adsense_publisher_code: client: true diff --git a/test/javascripts/acceptance/adsense-test.js.es6 b/test/javascripts/acceptance/adsense-test.js.es6 index fda87ca..2a9cbf2 100644 --- a/test/javascripts/acceptance/adsense-test.js.es6 +++ b/test/javascripts/acceptance/adsense-test.js.es6 @@ -1,8 +1,10 @@ import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers"; +import groupFixtures from "fixtures/group-fixtures"; acceptance("AdSense", { loggedIn: true, settings: { + no_ads_for_groups: "discourse", adsense_publisher_code: "MYADSENSEID", adsense_through_trust_level: 2, adsense_topic_list_top_code: "list_top_ad_unit", @@ -54,3 +56,17 @@ test("no ads for trust level 3", async assert => { "it should render 0 ads" ); }); + +QUnit.only("can omit ads based on groups", async assert => { + replaceCurrentUser({ + staff: false, + trust_level: 1, + groups: [groupFixtures["/groups/discourse.json"].group] + }); + await visit("/t/280"); + assert.equal( + find(".google-adsense.adsense-post-bottom").length, + 0, + "it should render 0 ads" + ); +}); diff --git a/test/javascripts/acceptance/dfp-test.js.es6 b/test/javascripts/acceptance/dfp-test.js.es6 new file mode 100644 index 0000000..9d58156 --- /dev/null +++ b/test/javascripts/acceptance/dfp-test.js.es6 @@ -0,0 +1,72 @@ +import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers"; +import groupFixtures from "fixtures/group-fixtures"; + +acceptance("DFP Ads", { + loggedIn: true, + settings: { + no_ads_for_groups: "discourse", + dfp_publisher_id: "MYdfpID", + dfp_through_trust_level: 2, + dfp_topic_list_top_code: "list_top_ad_unit", + dfp_topic_list_top_ad_sizes: "728*90 - leaderboard", + dfp_mobile_topic_list_top_code: "mobile_list_top_ad_unit", + dfp_mobile_topic_list_top_ad_size: "300*250 - medium rectangle", + dfp_post_bottom_code: "post_bottom_ad_unit", + dfp_post_bottom_ad_sizes: "728*90 - leaderboard", + 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 + } +}); + +test("correct number of ads should show", async assert => { + replaceCurrentUser({ staff: false, trust_level: 1 }); + await visit("/t/280"); // 20 posts + const ads = find(".google-dfp-ad.dfp-ad-post-bottom"); + assert.equal(ads.length, 3, "it should render 3 ads"); + assert.equal( + find("#post_6 + .widget-connector").find( + ".google-dfp-ad.dfp-ad-post-bottom" + ).length, + 1, + "ad after 6th post" + ); + assert.equal( + find("#post_12 + .widget-connector").find( + ".google-dfp-ad.dfp-ad-post-bottom" + ).length, + 1, + "ad after 12th post" + ); + assert.equal( + find("#post_18 + .widget-connector").find( + ".google-dfp-ad.dfp-ad-post-bottom" + ).length, + 1, + "ad after 18th post" + ); +}); + +test("no ads for trust level 3", async assert => { + replaceCurrentUser({ staff: false, trust_level: 3 }); + await visit("/t/280"); + assert.equal( + find(".google-dfp-ad.dfp-ad-post-bottom").length, + 0, + "it should render 0 ads" + ); +}); + +test("can omit ads based on groups", async assert => { + replaceCurrentUser({ + staff: false, + trust_level: 1, + groups: [groupFixtures["/groups/discourse.json"].group] + }); + await visit("/t/280"); + assert.equal( + find(".google-dfp-ad.dfp-ad-post-bottom").length, + 0, + "it should render 0 ads" + ); +});