mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-27 03:00:12 -04:00
Add frequency capping to ads
This commit is contained in:
@@ -89,6 +89,7 @@ from r2.lib.validator import (
|
||||
VDate,
|
||||
VExistingUname,
|
||||
VFloat,
|
||||
VFrequencyCap,
|
||||
VImageType,
|
||||
VInt,
|
||||
VLength,
|
||||
@@ -992,17 +993,23 @@ class PromoteApiController(ApiController):
|
||||
bid=VFloat('bid', coerce=False),
|
||||
target=VPromoTarget(),
|
||||
campaign_id36=nop("campaign_id36"),
|
||||
frequency_cap=VFrequencyCap(("frequency_capped",
|
||||
"frequency_cap",
|
||||
"frequency_cap_duration"),),
|
||||
priority=VPriority("priority"),
|
||||
location=VLocation(),
|
||||
platform=VOneOf("platform", ("mobile", "desktop", "all"), default="desktop"),
|
||||
mobile_os=VList("mobile_os", choices=["iOS", "Android"]),
|
||||
)
|
||||
def POST_edit_campaign(self, form, jquery, link, campaign_id36,
|
||||
start, end, bid, target, priority, location,
|
||||
platform, mobile_os):
|
||||
start, end, bid, target, frequency_cap,
|
||||
priority, location, platform, mobile_os):
|
||||
if not link:
|
||||
return
|
||||
|
||||
if form.has_errors('frequency_cap', errors.INVALID_FREQUENCY_CAP):
|
||||
return
|
||||
|
||||
if platform in ('mobile', 'all') and not mobile_os:
|
||||
c.errors.add(errors.BAD_PROMO_MOBILE_OS, field='mobile_os')
|
||||
form.set_error(errors.BAD_PROMO_MOBILE_OS, 'mobile_os')
|
||||
@@ -1142,9 +1149,11 @@ class PromoteApiController(ApiController):
|
||||
dates = (start, end)
|
||||
if campaign:
|
||||
promote.edit_campaign(link, campaign, dates, bid, cpm, target,
|
||||
priority, location, platform, mobile_os)
|
||||
frequency_cap[0], frequency_cap[1], priority,
|
||||
location, platform, mobile_os)
|
||||
else:
|
||||
campaign = promote.new_campaign(link, dates, bid, cpm, target,
|
||||
frequency_cap[0], frequency_cap[1],
|
||||
priority, location, platform, mobile_os)
|
||||
rc = RenderableCampaign.from_campaigns(link, campaign)
|
||||
jquery.update_campaign(campaign._fullname, rc.render_html())
|
||||
|
||||
@@ -152,6 +152,7 @@ error_list = dict((
|
||||
('JSON_MISSING_KEY', _('JSON missing key: "%(key)s"')),
|
||||
('NO_CHANGE_KIND', _("can't change post type")),
|
||||
('INVALID_LOCATION', _("invalid location")),
|
||||
('INVALID_FREQUENCY_CAP', _("invalid values for frequency cap")),
|
||||
('BANNED_FROM_SUBREDDIT', _('that user is banned from the subreddit')),
|
||||
('GOLD_REQUIRED', _('you must have an active reddit gold subscription to do that')),
|
||||
('INSUFFICIENT_CREDDITS', _("insufficient creddits")),
|
||||
|
||||
@@ -443,6 +443,7 @@ module["reddit-init-base"] = LocalizedModule("reddit-init-base.js",
|
||||
"lib/bootstrap.transition.js",
|
||||
"lib/bootstrap.tooltip.js",
|
||||
"lib/reddit-client-lib.js",
|
||||
"lib/jquery.cookie.js",
|
||||
"bootstrap.tooltip.extension.js",
|
||||
"base.js",
|
||||
"preload.js",
|
||||
@@ -481,7 +482,6 @@ module["reddit-init"] = LocalizedModule("reddit-init.js",
|
||||
)
|
||||
|
||||
module["reddit"] = LocalizedModule("reddit.js",
|
||||
"lib/jquery.cookie.js",
|
||||
"lib/jquery.url.js",
|
||||
"lib/backbone-1.0.0.js",
|
||||
"embed/custom-event.js",
|
||||
|
||||
@@ -279,10 +279,11 @@ def get_transactions(link, campaigns):
|
||||
bids_by_campaign = {c._id: bid_dict[(c._id, c.trans_id)] for c in campaigns}
|
||||
return bids_by_campaign
|
||||
|
||||
def new_campaign(link, dates, bid, cpm, target, priority, location,
|
||||
platform, mobile_os):
|
||||
def new_campaign(link, dates, bid, cpm, target, frequency_cap, frequency_cap_duration,
|
||||
priority, location, platform, mobile_os):
|
||||
campaign = PromoCampaign.create(link, target, bid, cpm, dates[0], dates[1],
|
||||
priority, location, platform, mobile_os)
|
||||
frequency_cap, frequency_cap_duration, priority,
|
||||
location, platform, mobile_os)
|
||||
PromotionWeights.add(link, campaign)
|
||||
PromotionLog.add(link, 'campaign %s created' % campaign._id)
|
||||
|
||||
@@ -298,8 +299,9 @@ def new_campaign(link, dates, bid, cpm, target, priority, location,
|
||||
def free_campaign(link, campaign, user):
|
||||
auth_campaign(link, campaign, user, -1)
|
||||
|
||||
def edit_campaign(link, campaign, dates, bid, cpm, target, priority, location,
|
||||
platform='desktop', mobile_os=None):
|
||||
def edit_campaign(link, campaign, dates, bid, cpm, target, frequency_cap,
|
||||
frequency_cap_duration, priority, location, platform='desktop',
|
||||
mobile_os=None):
|
||||
changed = {}
|
||||
if bid != campaign.bid:
|
||||
# if the bid amount changed, cancel any pending transactions
|
||||
@@ -320,6 +322,13 @@ def edit_campaign(link, campaign, dates, bid, cpm, target, priority, location,
|
||||
if target != campaign.target:
|
||||
changed['target'] = (campaign.target, target)
|
||||
campaign.target = target
|
||||
if frequency_cap != campaign.frequency_cap:
|
||||
changed['frequency_cap'] = (campaign.frequency_cap, frequency_cap)
|
||||
campaign.frequency_cap = frequency_cap
|
||||
if frequency_cap_duration != campaign.frequency_cap_duration:
|
||||
changed['frequency_cap_duration'] = (campaign.frequency_cap_duration,
|
||||
frequency_cap_duration)
|
||||
campaign.frequency_cap_duration = frequency_cap_duration
|
||||
if priority != campaign.priority:
|
||||
changed['priority'] = (campaign.priority.name, priority.name)
|
||||
campaign.priority = priority
|
||||
|
||||
@@ -2128,6 +2128,22 @@ class VList(Validator):
|
||||
return {self.param: docs}
|
||||
|
||||
|
||||
class VFrequencyCap(Validator):
|
||||
def run(self, frequency_capped='false', frequency_cap=None,
|
||||
frequency_cap_duration=None):
|
||||
|
||||
if frequency_capped == 'true':
|
||||
if frequency_cap and frequency_cap_duration:
|
||||
try:
|
||||
return (int(frequency_cap), int(frequency_cap_duration),)
|
||||
except (ValueError, TypeError):
|
||||
self.set_error(errors.INVALID_FREQUENCY_CAP, code=400)
|
||||
else:
|
||||
self.set_error(errors.INVALID_FREQUENCY_CAP, code=400)
|
||||
else:
|
||||
return (None, None)
|
||||
|
||||
|
||||
class VPriority(Validator):
|
||||
def run(self, val):
|
||||
if c.user_is_sponsor:
|
||||
|
||||
@@ -311,6 +311,8 @@ class PromoCampaign(Thing):
|
||||
location_code=None,
|
||||
platform='desktop',
|
||||
mobile_os_names=None,
|
||||
frequency_cap=None,
|
||||
frequency_cap_duration=None,
|
||||
)
|
||||
|
||||
# special attributes that shouldn't set Thing data attributes because they
|
||||
@@ -376,8 +378,8 @@ class PromoCampaign(Thing):
|
||||
return target_sr_names, target_name
|
||||
|
||||
@classmethod
|
||||
def create(cls, link, target, bid, cpm, start_date, end_date, priority,
|
||||
location, platform, mobile_os):
|
||||
def create(cls, link, target, bid, cpm, start_date, end_date, frequency_cap,
|
||||
frequency_cap_duration, priority, location, platform, mobile_os):
|
||||
pc = PromoCampaign(
|
||||
link_id=link._id,
|
||||
bid=bid,
|
||||
@@ -387,6 +389,8 @@ class PromoCampaign(Thing):
|
||||
trans_id=NO_TRANSACTION,
|
||||
owner_id=link.author_id,
|
||||
)
|
||||
pc.frequency_cap = frequency_cap
|
||||
pc.frequency_cap_duration = frequency_cap_duration
|
||||
pc.priority = priority
|
||||
pc.location = location
|
||||
pc.target = target
|
||||
|
||||
@@ -5614,6 +5614,21 @@ ul.colors {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.frequency-cap-inputs {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.frequency-cap-example {
|
||||
margin: -15px 0 0 20px;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.frequency-cap-dur-unit {
|
||||
vertical-align: sub;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.minimum-spend.error {
|
||||
font-weight: bold;
|
||||
color: red;
|
||||
@@ -5705,7 +5720,8 @@ ul.colors {
|
||||
.campaign td.prefright {
|
||||
padding: 8px 4px 4px;
|
||||
}
|
||||
.campaign #bid, .campaign #impressions {
|
||||
.campaign #bid, .campaign #impressions,
|
||||
.campaign #cap, .campaign #duration {
|
||||
text-align: right;
|
||||
width: auto;
|
||||
margin-bottom: 5px;
|
||||
@@ -6595,7 +6611,8 @@ div #campaign-field {
|
||||
|
||||
.lookup-user-field,
|
||||
.budget-field,
|
||||
.timing-field {
|
||||
.timing-field,
|
||||
.frequency-cap-field {
|
||||
.group {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
@@ -598,6 +598,10 @@ var exports = r.sponsored = {
|
||||
collapse();
|
||||
},
|
||||
|
||||
toggleFrequency: function() {
|
||||
$('.frequency-cap-field').toggle('slow');
|
||||
},
|
||||
|
||||
setup_geotargeting: function(regions, metros) {
|
||||
this.regions = regions
|
||||
this.metros = metros
|
||||
@@ -1570,7 +1574,8 @@ function edit_campaign($campaign_row) {
|
||||
$editRow.append($editCell)
|
||||
$campaign_row.fadeOut(function() {
|
||||
/* fill inputs from data in campaign row */
|
||||
_.each(['startdate', 'enddate', 'bid', 'campaign_id36', 'campaign_name'],
|
||||
_.each(['startdate', 'enddate', 'bid', 'campaign_id36', 'campaign_name',
|
||||
'frequency_cap', 'frequency_cap_duration'],
|
||||
function(input) {
|
||||
var val = $campaign_row.data(input),
|
||||
$input = campaign.find('*[name="' + input + '"]')
|
||||
@@ -1589,6 +1594,12 @@ function edit_campaign($campaign_row) {
|
||||
});
|
||||
}
|
||||
|
||||
/* show frequency inputs */
|
||||
if ($campaign_row.data('frequency_cap')) {
|
||||
$('.frequency-cap-field').show();
|
||||
$('#frequency_capped_true').prop('checked', 'checked');
|
||||
}
|
||||
|
||||
/* set priority */
|
||||
var priorities = campaign.find('*[name="priority"]'),
|
||||
campPriority = $campaign_row.data("priority")
|
||||
@@ -1693,6 +1704,10 @@ function create_campaign() {
|
||||
.find('select[name="country"]').val('').end()
|
||||
.find('select[name="region"]').hide().end()
|
||||
.find('select[name="metro"]').hide().end()
|
||||
.find('input[name="frequency_cap"]').val('').end()
|
||||
.find('input[name="frequency_cap_duration"]').val('').end()
|
||||
.find('#frequency_capped_false').prop('checked', 'checked').end()
|
||||
.find('.frequency-cap-field').hide().end()
|
||||
.slideDown();
|
||||
r.sponsored.render();
|
||||
});
|
||||
|
||||
@@ -3,15 +3,29 @@
|
||||
setup: function(organicLinks, interestProb, showPromo, srnames) {
|
||||
this.organics = [];
|
||||
this.lineup = [];
|
||||
this.adWasClicked = false;
|
||||
this.interestProb = interestProb;
|
||||
this.showPromo = showPromo;
|
||||
this.srnames = srnames;
|
||||
this.lastPromoTimestamp = Date.now();
|
||||
this.MIN_PROMO_TIME = 1500;
|
||||
this.loid = $.cookie('loid');
|
||||
this.lastTabChangeTimestamp = Date.now();
|
||||
this.MIN_PROMO_TIME = 3000;
|
||||
this.next = this._advance.bind(this, 1);
|
||||
this.prev = this._advance.bind(this, -1);
|
||||
this.$listing = $('.organic-listing');
|
||||
|
||||
this.$listing.on('click', function(e) {
|
||||
var $target = $(e.target);
|
||||
if ($target.is('.thumbnail, .title')) {
|
||||
this.adWasClicked = true;
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.adBlockIsEnabled = $('#siteTable_organic').is(":hidden");
|
||||
if (this.adBlockIsEnabled) {
|
||||
this.showPromo = false;
|
||||
}
|
||||
|
||||
organicLinks.forEach(function(name) {
|
||||
this.organics.push(name);
|
||||
this.lineup.push({ fullname: name, });
|
||||
@@ -36,7 +50,8 @@
|
||||
r.debug('restoring spotlight selection to last click');
|
||||
selectedThing = { fullname: lastClickFullname, };
|
||||
} else {
|
||||
selectedThing = this.chooseRandom();
|
||||
var shouldForcePromo = this._isDocumentVisible() && this.showPromo;
|
||||
selectedThing = this.chooseRandom(shouldForcePromo);
|
||||
}
|
||||
|
||||
this.lineup = _.chain(this.lineup)
|
||||
@@ -50,40 +65,56 @@
|
||||
this.lineup.pos = 0;
|
||||
this._advance(0);
|
||||
|
||||
if ('hidden' in document) {
|
||||
this.readyForNewPromo = !document.hidden;
|
||||
if (!this.showPromo) {
|
||||
return;
|
||||
}
|
||||
// IE 9 and below do not have this prop or work with
|
||||
// visibilitychange.
|
||||
if ('hidden' in document ) {
|
||||
$(document).on('visibilitychange', this._requestOrSaveTimestamp.bind(this));
|
||||
} else {
|
||||
$(window).on('focus blur', this._requestOrSaveTimestamp.bind(this));
|
||||
}
|
||||
|
||||
$(document).on('visibilitychange', function(e) {
|
||||
if (!document.hidden) {
|
||||
this.requestNewPromo();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_requestOrSaveTimestamp: function() {
|
||||
if ( this._isDocumentVisible() ) {
|
||||
this.requestNewPromo();
|
||||
} else {
|
||||
this.readyForNewPromo = document.hasFocus();
|
||||
this.lastTabChangeTimestamp = Date.now();
|
||||
}
|
||||
},
|
||||
|
||||
$(window).on('focus', this.requestNewPromo.bind(this));
|
||||
_isDocumentVisible: function () {
|
||||
if ('hidden' in document) {
|
||||
return !document.hidden;
|
||||
} else {
|
||||
return document.hasFocus();
|
||||
}
|
||||
},
|
||||
|
||||
requestNewPromo: function() {
|
||||
// if the page loads in a background tab, this should be false. In that
|
||||
// case, we don't want to load a new ad, as this will be the first view
|
||||
if (!this.readyForNewPromo) {
|
||||
this.readyForNewPromo = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// the ad will be stored as a promise
|
||||
if (!this.lineup[this.lineup.pos].promise) {
|
||||
return;
|
||||
}
|
||||
// we don't want to fetch a new ad when the user has clicked so they
|
||||
// can have a chance to vote or comment on the last ad.
|
||||
if (this.adWasClicked) {
|
||||
return;
|
||||
}
|
||||
|
||||
var $promotedLink = this.$listing.find('.promotedlink');
|
||||
var $clearLeft = $promotedLink.next('.clearleft');
|
||||
|
||||
if (!$promotedLink.length || $promotedLink.is(':hidden') ||
|
||||
$promotedLink.offset().top < window.scrollY ||
|
||||
Date.now() - this.lastPromoTimestamp < this.MIN_PROMO_TIME) {
|
||||
if (this.adBlockIsEnabled ||
|
||||
Date.now() - this.lastTabChangeTimestamp < this.MIN_PROMO_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($promotedLink.length && $promotedLink.offset().top < window.scrollY) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,10 +127,22 @@
|
||||
var $link = $promo.eq(0);
|
||||
var fullname = $link.data('fullname');
|
||||
|
||||
this.organics[this.lineup.pos] = fullname;
|
||||
this.lineup[this.lineup.pos] = newPromo;
|
||||
$promotedLink.add($clearLeft).remove();
|
||||
$promo.show();
|
||||
if ($promotedLink.length) {
|
||||
this.organics[this.lineup.pos] = fullname;
|
||||
this.lineup[this.lineup.pos] = newPromo;
|
||||
} else {
|
||||
this.organics[this.lineup.pos + 1] = fullname;
|
||||
this.lineup[this.lineup.pos + 1] = newPromo;
|
||||
}
|
||||
|
||||
if (!$link.hasClass('adsense-wrap')) {
|
||||
if ($promotedLink.length) {
|
||||
$promotedLink.add($clearLeft).remove();
|
||||
$promo.show();
|
||||
} else {
|
||||
this.next()
|
||||
}
|
||||
}
|
||||
// force a redraw to prevent showing duplicate ads
|
||||
this.$listing.hide().show();
|
||||
}.bind(this));
|
||||
@@ -113,27 +156,44 @@
|
||||
data: {
|
||||
srnames: this.srnames,
|
||||
r: r.config.post_site,
|
||||
loid: this.loid,
|
||||
},
|
||||
}).pipe(function(promo) {
|
||||
var prevPromo = this.$listing.find('.promotedlink')
|
||||
if (promo) {
|
||||
this.lastPromoTimestamp = Date.now();
|
||||
if (this.showPromo) {
|
||||
$('#siteTable_organic').show('slow');
|
||||
}
|
||||
|
||||
var $item = $(promo);
|
||||
$item.hide().appendTo(this.$listing);
|
||||
// adsense will throw error if inserted while hidden
|
||||
if (!$item.hasClass('adsense-wrap')) {
|
||||
$item.hide().appendTo(this.$listing);
|
||||
} else {
|
||||
var $promotedLink = this.$listing.find('.promotedlink');
|
||||
$promotedLink.remove()
|
||||
$item.appendTo(this.$listing);
|
||||
}
|
||||
return $item;
|
||||
} else {
|
||||
if (!prevPromo.length && !this.organics.length) {
|
||||
// spotlight box must be hidden when no ad is returned
|
||||
// and there is no other content.
|
||||
$('#siteTable_organic').hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
chooseRandom: function() {
|
||||
if (this.showPromo) {
|
||||
chooseRandom: function(forcePromo) {
|
||||
if (forcePromo) {
|
||||
return this.requestPromo();
|
||||
} else if (Math.random() < this.interestProb) {
|
||||
return '.interestbar';
|
||||
} else {
|
||||
var name = this.organics[Math.floor(Math.random() * this.organics.length)];
|
||||
return { fullname: name, };
|
||||
var name = this.organics[_.random(this.organics.length)];
|
||||
return (name) ? { fullname: name, } : null;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -238,6 +298,8 @@
|
||||
$help.find('.help-promoted').show();
|
||||
} else if ($thing.hasClass('interestbar')) {
|
||||
$help.find('.help-interestbar').show();
|
||||
} else if ($thing.hasClass('adsense-wrap')) {
|
||||
$help.find('.help-adserver').show()
|
||||
} else {
|
||||
$help.find('.help-organic').show();
|
||||
}
|
||||
|
||||
@@ -426,6 +426,78 @@
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
<%def name="frequency_cap_field(default_checked='false')">
|
||||
<%def name="frequency_select_content()">
|
||||
<div class="radio-group group">
|
||||
<span class="label">frequency capping</span>
|
||||
<label class="form-group">
|
||||
<input id="frequency_capped_false" class="nomargin"
|
||||
type="radio" value="false" name="frequency_capped"
|
||||
onclick="r.sponsored.toggleFrequency()"
|
||||
%if default_checked == 'false':
|
||||
checked="checked"
|
||||
%endif
|
||||
>
|
||||
<div class="label-group">
|
||||
<span class="label">${_("no frequency cap")}</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="form-group">
|
||||
<input id="frequency_capped_true" class="nomargin"
|
||||
type="radio" value="true" name="frequency_capped"
|
||||
onclick="r.sponsored.toggleFrequency()"
|
||||
%if default_checked == 'true':
|
||||
checked="checked"
|
||||
%endif
|
||||
>
|
||||
<div class="label-group">
|
||||
<span class="label">${_("frequency capped")}</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="frequency_details_content()">
|
||||
<div class="group frequency-cap-inputs">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<span class="label">${_("cap")}</span>
|
||||
<div class="input-group">
|
||||
<input id="frequency_cap" name="frequency_cap" size="7" type="text"
|
||||
class="rounded style-input"
|
||||
style="width:auto"/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<span class="label">${_("duration")}</span>
|
||||
<div class="input-group">
|
||||
<input id="frequency_cap_duration" name="frequency_cap_duration"
|
||||
size="7" type="text" class="rounded styled-input"/>
|
||||
</div>
|
||||
<span class="frequency-cap-dur-unit">hours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="frequency-cap-example">
|
||||
${_('Example: Display this flight 3 times per 24 hours.')}
|
||||
</div>
|
||||
${error_field("INVALID_FREQUENCY_CAP", "frequency_cap", "div")}
|
||||
</%def>
|
||||
|
||||
%if c.user_is_sponsor:
|
||||
<%utils:line_field title="${_('frequency')}" css_class="rounded">
|
||||
${frequency_select_content()}
|
||||
<div class="frequency-cap-field hidden">
|
||||
${frequency_details_content()}
|
||||
</div>
|
||||
</%utils:line_field>
|
||||
%endif
|
||||
</%def>
|
||||
|
||||
<%def name="priority_field()">
|
||||
<%def name="priority_field_content()">
|
||||
<div class="radio-group">
|
||||
|
||||
@@ -94,6 +94,7 @@ ${pr.javascript_setup()}
|
||||
${pr.targeting_field()}
|
||||
${pr.timing_field()}
|
||||
${pr.platform_field()}
|
||||
${pr.frequency_cap_field()}
|
||||
${pr.priority_field()}
|
||||
${pr.budget_field()}
|
||||
<%utils:line_field title="${_('confirm')}" css_class="rounded confirmation-field">
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
data-cpm="${getattr(thing.campaign, 'cpm', g.cpm_selfserve.pennies)}"
|
||||
data-campaign_id36="${thing.campaign._id36}"
|
||||
data-campaign_name="${thing.campaign._fullname}"
|
||||
data-frequency_cap="${thing.campaign.frequency_cap}"
|
||||
data-frequency_cap_duration="${thing.campaign.frequency_cap_duration}"
|
||||
data-priority="${thing.campaign.priority.name}"
|
||||
data-override="${json.dumps(thing.campaign.priority.inventory_override)}"
|
||||
data-platform="${thing.platform}"
|
||||
|
||||
Reference in New Issue
Block a user