diff --git a/app/controllers/house_ad_settings_controller.rb b/app/controllers/house_ad_settings_controller.rb new file mode 100644 index 0000000..62d6b73 --- /dev/null +++ b/app/controllers/house_ad_settings_controller.rb @@ -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 diff --git a/app/controllers/house_ads_controller.rb b/app/controllers/house_ads_controller.rb new file mode 100644 index 0000000..b596180 --- /dev/null +++ b/app/controllers/house_ads_controller.rb @@ -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 diff --git a/app/models/house_ad.rb b/app/models/house_ad.rb new file mode 100644 index 0000000..ab467de --- /dev/null +++ b/app/models/house_ad.rb @@ -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 = "
New Ad
" + 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 diff --git a/app/models/house_ad_setting.rb b/app/models/house_ad_setting.rb new file mode 100644 index 0000000..fac5a02 --- /dev/null +++ b/app/models/house_ad_setting.rb @@ -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 diff --git a/assets/javascripts/discourse/adplugin-route-map.js.es6 b/assets/javascripts/discourse/adplugin-route-map.js.es6 new file mode 100644 index 0000000..bd72f49 --- /dev/null +++ b/assets/javascripts/discourse/adplugin-route-map.js.es6 @@ -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" }); + }); + } +}; diff --git a/assets/javascripts/discourse/components/ad_component.js.es6 b/assets/javascripts/discourse/components/ad-component.js.es6 similarity index 72% rename from assets/javascripts/discourse/components/ad_component.js.es6 rename to assets/javascripts/discourse/components/ad-component.js.es6 index 13f0799..c70cbb1 100644 --- a/assets/javascripts/discourse/components/ad_component.js.es6 +++ b/assets/javascripts/discourse/components/ad-component.js.es6 @@ -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; + } } }); diff --git a/assets/javascripts/discourse/components/ad-slot.js.es6 b/assets/javascripts/discourse/components/ad-slot.js.es6 new file mode 100644 index 0000000..297e224 --- /dev/null +++ b/assets/javascripts/discourse/components/ad-slot.js.es6 @@ -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]); + }); + } +}); diff --git a/assets/javascripts/discourse/components/amazon-product-links.js.es6 b/assets/javascripts/discourse/components/amazon-product-links.js.es6 index 532fcf0..53e41c3 100644 --- a/assets/javascripts/discourse/components/amazon-product-links.js.es6 +++ b/assets/javascripts/discourse/components/amazon-product-links.js.es6 @@ -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)); } }); diff --git a/assets/javascripts/discourse/components/carbonads-ad.js.es6 b/assets/javascripts/discourse/components/carbonads-ad.js.es6 index 1571399..6bd49d6 100644 --- a/assets/javascripts/discourse/components/carbonads-ad.js.es6 +++ b/assets/javascripts/discourse/components/carbonads-ad.js.es6 @@ -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 diff --git a/assets/javascripts/discourse/components/codefund-ad.js.es6 b/assets/javascripts/discourse/components/codefund-ad.js.es6 index 9c26db8..973157d 100644 --- a/assets/javascripts/discourse/components/codefund-ad.js.es6 +++ b/assets/javascripts/discourse/components/codefund-ad.js.es6 @@ -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)); } }); diff --git a/assets/javascripts/discourse/components/google-adsense.js.es6 b/assets/javascripts/discourse/components/google-adsense.js.es6 index e124747..3b31018 100644 --- a/assets/javascripts/discourse/components/google-adsense.js.es6 +++ b/assets/javascripts/discourse/components/google-adsense.js.es6 @@ -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)); } }); diff --git a/assets/javascripts/discourse/components/google-dfp-ad.js.es6 b/assets/javascripts/discourse/components/google-dfp-ad.js.es6 index b213f85..4e5a3ae 100755 --- a/assets/javascripts/discourse/components/google-dfp-ad.js.es6 +++ b/assets/javascripts/discourse/components/google-dfp-ad.js.es6 @@ -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")]; diff --git a/assets/javascripts/discourse/components/house-ad.js.es6 b/assets/javascripts/discourse/components/house-ad.js.es6 new file mode 100644 index 0000000..726110b --- /dev/null +++ b/assets/javascripts/discourse/components/house-ad.js.es6 @@ -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(); + } +}); diff --git a/assets/javascripts/discourse/components/house-ads-chooser.js.es6 b/assets/javascripts/discourse/components/house-ads-chooser.js.es6 new file mode 100644 index 0000000..f574bc9 --- /dev/null +++ b/assets/javascripts/discourse/components/house-ads-chooser.js.es6 @@ -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); + } +}); diff --git a/assets/javascripts/discourse/components/house-ads-list-setting.js.es6 b/assets/javascripts/discourse/components/house-ads-list-setting.js.es6 new file mode 100644 index 0000000..69133f7 --- /dev/null +++ b/assets/javascripts/discourse/components/house-ads-list-setting.js.es6 @@ -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") +}); diff --git a/assets/javascripts/discourse/components/house-ads-setting.js.es6 b/assets/javascripts/discourse/components/house-ads-setting.js.es6 new file mode 100644 index 0000000..ae87fa5 --- /dev/null +++ b/assets/javascripts/discourse/components/house-ads-setting.js.es6 @@ -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")); + } + } +}); diff --git a/assets/javascripts/discourse/controllers/admin-plugins-house-ads-index.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-house-ads-index.js.es6 new file mode 100644 index 0000000..9322e96 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-plugins-house-ads-index.js.es6 @@ -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") +}); diff --git a/assets/javascripts/discourse/controllers/admin-plugins-house-ads-show.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-house-ads-show.js.es6 new file mode 100644 index 0000000..106d9b8 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-plugins-house-ads-show.js.es6 @@ -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"))); + } + } +}); diff --git a/assets/javascripts/discourse/controllers/admin-plugins-house-ads.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-house-ads.js.es6 new file mode 100644 index 0000000..8b22942 --- /dev/null +++ b/assets/javascripts/discourse/controllers/admin-plugins-house-ads.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Controller.extend({ + loadingAds: true +}); diff --git a/assets/javascripts/discourse/routes/admin-plugins-house-ads-index.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-house-ads-index.js.es6 new file mode 100644 index 0000000..6610e77 --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-plugins-house-ads-index.js.es6 @@ -0,0 +1,7 @@ +export default Discourse.Route.extend({ + actions: { + moreSettings() { + this.transitionTo("adminSiteSettingsCategory", "ad_plugin"); + } + } +}); diff --git a/assets/javascripts/discourse/routes/admin-plugins-house-ads-show.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-house-ads-show.js.es6 new file mode 100644 index 0000000..d82f86a --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-plugins-house-ads-show.js.es6 @@ -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) + ); + } + } +}); diff --git a/assets/javascripts/discourse/routes/admin-plugins-house-ads.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-house-ads.js.es6 new file mode 100644 index 0000000..cf41b62 --- /dev/null +++ b/assets/javascripts/discourse/routes/admin-plugins-house-ads.js.es6 @@ -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 + }); + } +}); diff --git a/assets/javascripts/discourse/templates/admin/plugins-house-ads-index.hbs b/assets/javascripts/discourse/templates/admin/plugins-house-ads-index.hbs new file mode 100644 index 0000000..3284a0c --- /dev/null +++ b/assets/javascripts/discourse/templates/admin/plugins-house-ads-index.hbs @@ -0,0 +1,23 @@ +{{#d-section class="house-ads-settings content-body"}} +
{{i18n 'admin.adplugin.house_ads.description'}}
+ + {{#unless houseAds.length}} +

+ {{#link-to 'adminPlugins.houseAds.show' 'new'}} + {{i18n 'admin.adplugin.house_ads.get_started'}} + {{/link-to}} +

+ {{else}} +
+ {{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")}} +
+ {{/unless}} +{{/d-section}} diff --git a/assets/javascripts/discourse/templates/admin/plugins-house-ads-show.hbs b/assets/javascripts/discourse/templates/admin/plugins-house-ads-show.hbs new file mode 100644 index 0000000..10b553e --- /dev/null +++ b/assets/javascripts/discourse/templates/admin/plugins-house-ads-show.hbs @@ -0,0 +1,26 @@ +{{#d-section class="edit-house-ad content-body"}} +

{{text-field class="house-ad-name" value=buffered.name}}

+
+ {{ace-editor content=buffered.html mode="html"}} +
+
+ {{d-button + action=(action "save") + disabled=disableSave + class="btn-primary" + label="admin.adplugin.house_ads.save"}} + + {{#if saving}} + {{savingStatus}} + {{else}} + {{#if dirty}} + {{i18n 'cancel'}} + {{/if}} + {{/if}} + + {{d-button + action=(action "destroy") + class="btn-danger delete-button" + label="admin.adplugin.house_ads.delete"}} +
+{{/d-section}} diff --git a/assets/javascripts/discourse/templates/admin/plugins-house-ads.hbs b/assets/javascripts/discourse/templates/admin/plugins-house-ads.hbs new file mode 100644 index 0000000..75e2c70 --- /dev/null +++ b/assets/javascripts/discourse/templates/admin/plugins-house-ads.hbs @@ -0,0 +1,27 @@ +
+

{{i18n 'admin.adplugin.house_ads.title'}}

+ {{#if model.length}} +
+
+ {{#link-to 'adminPlugins.houseAds.show' 'new' class="btn btn-primary"}} + {{d-icon "plus"}} + {{i18n 'admin.adplugin.house_ads.new'}} + {{/link-to}} + {{#link-to 'adminPlugins.houseAds.index' class="btn btn-default"}} + {{d-icon "cog"}} + {{i18n 'admin.adplugin.house_ads.settings'}} + {{/link-to}} +
+ +
+ {{/if}} + {{outlet}} +
diff --git a/assets/javascripts/discourse/templates/components/ad-slot.hbs b/assets/javascripts/discourse/templates/components/ad-slot.hbs new file mode 100644 index 0000000..7a49e31 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/ad-slot.hbs @@ -0,0 +1,8 @@ +{{#each adComponents as |adComponent|}} + {{component adComponent + placement=placement + refreshOnChange=refreshOnChange + category=category + listLoading=listLoading + postNumber=postNumber}} +{{/each}} diff --git a/assets/javascripts/discourse/templates/components/adplugin-container.hbs b/assets/javascripts/discourse/templates/components/adplugin-container.hbs deleted file mode 100644 index bcffef5..0000000 --- a/assets/javascripts/discourse/templates/components/adplugin-container.hbs +++ /dev/null @@ -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}} diff --git a/assets/javascripts/discourse/templates/components/house-ad.hbs b/assets/javascripts/discourse/templates/components/house-ad.hbs new file mode 100644 index 0000000..7e97aa9 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/house-ad.hbs @@ -0,0 +1,3 @@ +{{#if showAd}} + {{{adHtml}}} +{{/if}} diff --git a/assets/javascripts/discourse/templates/components/house-ads-list-setting.hbs b/assets/javascripts/discourse/templates/components/house-ads-list-setting.hbs new file mode 100644 index 0000000..02bf05d --- /dev/null +++ b/assets/javascripts/discourse/templates/components/house-ads-list-setting.hbs @@ -0,0 +1,9 @@ + +{{house-ads-chooser settingValue=adValue choices=adNames}} +
+ {{#if changed}} + {{d-button class="ok" action=(action "save") icon="check"}} + {{d-button class="cancel" action=(action "cancel") icon="times"}} + {{/if}} +
+

{{help}}

diff --git a/assets/javascripts/discourse/templates/components/house-ads-setting.hbs b/assets/javascripts/discourse/templates/components/house-ads-setting.hbs new file mode 100644 index 0000000..b98b53f --- /dev/null +++ b/assets/javascripts/discourse/templates/components/house-ads-setting.hbs @@ -0,0 +1,9 @@ + +{{text-field value=adValue classNames="house-ads-text-input"}} +
+ {{#if changed}} + {{d-button class="ok" action=(action "save") icon="check"}} + {{d-button class="cancel" action=(action "cancel") icon="times"}} + {{/if}} +
+

{{help}}

diff --git a/assets/javascripts/discourse/templates/components/post-bottom-ad.hbs b/assets/javascripts/discourse/templates/components/post-bottom-ad.hbs new file mode 100644 index 0000000..7709c7c --- /dev/null +++ b/assets/javascripts/discourse/templates/components/post-bottom-ad.hbs @@ -0,0 +1 @@ +{{ad-slot placement="post-bottom" category=model.topic.category.slug postNumber=model.post_number}} diff --git a/assets/javascripts/discourse/templates/connectors/discovery-list-container-top/discourse-adplugin.hbs b/assets/javascripts/discourse/templates/connectors/discovery-list-container-top/discourse-adplugin.hbs index 3a50085..eec8db3 100644 --- a/assets/javascripts/discourse/templates/connectors/discovery-list-container-top/discourse-adplugin.hbs +++ b/assets/javascripts/discourse/templates/connectors/discovery-list-container-top/discourse-adplugin.hbs @@ -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}} \ No newline at end of file +{{ad-slot placement="topic-list-top" refreshOnChange=listLoading category=category.slug listLoading=listLoading}} diff --git a/assets/javascripts/discourse/templates/connectors/post-bottom/discourse-adplugin.hbs b/assets/javascripts/discourse/templates/connectors/post-bottom/discourse-adplugin.hbs index ab5aa7d..f0287d1 100644 --- a/assets/javascripts/discourse/templates/connectors/post-bottom/discourse-adplugin.hbs +++ b/assets/javascripts/discourse/templates/connectors/post-bottom/discourse-adplugin.hbs @@ -1 +1 @@ -{{adplugin-container model=this}} +{{post-bottom-ad model=this}} diff --git a/assets/javascripts/discourse/templates/connectors/topic-above-post-stream/discourse-adplugin.hbs b/assets/javascripts/discourse/templates/connectors/topic-above-post-stream/discourse-adplugin.hbs index e5d3d74..eea3ea0 100644 --- a/assets/javascripts/discourse/templates/connectors/topic-above-post-stream/discourse-adplugin.hbs +++ b/assets/javascripts/discourse/templates/connectors/topic-above-post-stream/discourse-adplugin.hbs @@ -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}} \ No newline at end of file +{{ad-slot placement="topic-above-post-stream" refreshOnChange=model.id category=model.category.slug}} diff --git a/assets/javascripts/discourse/templates/connectors/topic-above-suggested/discourse-adplugin.hbs b/assets/javascripts/discourse/templates/connectors/topic-above-suggested/discourse-adplugin.hbs index 8f45084..a90622d 100644 --- a/assets/javascripts/discourse/templates/connectors/topic-above-suggested/discourse-adplugin.hbs +++ b/assets/javascripts/discourse/templates/connectors/topic-above-suggested/discourse-adplugin.hbs @@ -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}} \ No newline at end of file +{{ad-slot placement="topic-above-suggested" refreshOnChange=model.id category=model.category.slug}} diff --git a/assets/javascripts/initializers/initialize-ad-plugin.js.es6 b/assets/javascripts/initializers/initialize-ad-plugin.js.es6 index a6315b8..e42915c 100644 --- a/assets/javascripts/initializers/initialize-ad-plugin.js.es6 +++ b/assets/javascripts/initializers/initialize-ad-plugin.js.es6 @@ -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); + }); } }; diff --git a/assets/stylesheets/adplugin.scss b/assets/stylesheets/adplugin.scss index 6497d14..9e7bad3 100644 --- a/assets/stylesheets/adplugin.scss +++ b/assets/stylesheets/adplugin.scss @@ -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; + } + } + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e45b924..4448240 100755 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index cfde9d4..0426c06 100755 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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." diff --git a/config/settings.yml b/config/settings.yml index dfc4004..d3f220d 100755 --- a/config/settings.yml +++ b/config/settings.yml @@ -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: diff --git a/plugin.rb b/plugin.rb index ef620d7..362873a 100755 --- a/plugin.rb +++ b/plugin.rb @@ -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 diff --git a/spec/models/house_ad_setting_spec.rb b/spec/models/house_ad_setting_spec.rb new file mode 100644 index 0000000..28cfe15 --- /dev/null +++ b/spec/models/house_ad_setting_spec.rb @@ -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: "

Banner

") + AdPlugin::HouseAd.create(name: "Donate", html: "

Donate

") + 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, '