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:
parent
5272995e74
commit
1bd80e1afe
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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" });
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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")];
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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")
|
||||
});
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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")
|
||||
});
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
export default Ember.Controller.extend({
|
||||
loadingAds: true
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
export default Discourse.Route.extend({
|
||||
actions: {
|
||||
moreSettings() {
|
||||
this.transitionTo("adminSiteSettingsCategory", "ad_plugin");
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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
|
||||
});
|
||||
}
|
||||
});
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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>
|
|
@ -0,0 +1,8 @@
|
|||
{{#each adComponents as |adComponent|}}
|
||||
{{component adComponent
|
||||
placement=placement
|
||||
refreshOnChange=refreshOnChange
|
||||
category=category
|
||||
listLoading=listLoading
|
||||
postNumber=postNumber}}
|
||||
{{/each}}
|
|
@ -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}}
|
|
@ -0,0 +1,3 @@
|
|||
{{#if showAd}}
|
||||
{{{adHtml}}}
|
||||
{{/if}}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
{{ad-slot placement="post-bottom" category=model.topic.category.slug postNumber=model.post_number}}
|
|
@ -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}}
|
||||
|
|
|
@ -1 +1 @@
|
|||
{{adplugin-container model=this}}
|
||||
{{post-bottom-ad model=this}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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:
|
||||
|
|
41
plugin.rb
41
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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: {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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: {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue