mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-04-05 03:00:15 -04:00
Add geotargeting for selfserve advertising.
This commit is contained in:
@@ -311,6 +311,7 @@ sponsors =
|
||||
selfserve_support_email = selfservesupport@mydomain.com
|
||||
MAX_CAMPAIGNS_PER_LINK = 100
|
||||
cpm_selfserve = 1.00
|
||||
cpm_selfserve_geotarget = 0.25
|
||||
adserver_click_domain =
|
||||
|
||||
# authorize.net credentials (blank authorizenetapi to disable)
|
||||
|
||||
@@ -75,6 +75,7 @@ from r2.lib.validator import (
|
||||
VInt,
|
||||
VLength,
|
||||
VLink,
|
||||
VLocation,
|
||||
VModhash,
|
||||
VOneOf,
|
||||
VPriority,
|
||||
@@ -113,15 +114,16 @@ def campaign_has_oversold_error(form, campaign):
|
||||
target = Subreddit._by_name(campaign.sr_name) if campaign.sr_name else None
|
||||
return has_oversold_error(form, campaign, campaign.start_date,
|
||||
campaign.end_date, campaign.bid, campaign.cpm,
|
||||
target)
|
||||
target, campaign.location)
|
||||
|
||||
|
||||
def has_oversold_error(form, campaign, start, end, bid, cpm, target):
|
||||
def has_oversold_error(form, campaign, start, end, bid, cpm, target, location):
|
||||
ndays = (to_date(end) - to_date(start)).days
|
||||
total_request = calc_impressions(bid, cpm)
|
||||
daily_request = int(total_request / ndays)
|
||||
oversold = inventory.get_oversold(target or Frontpage, start, end,
|
||||
daily_request, ignore=campaign)
|
||||
daily_request, ignore=campaign,
|
||||
location=location)
|
||||
|
||||
if oversold:
|
||||
min_daily = min(oversold.values())
|
||||
@@ -296,13 +298,18 @@ class PromoteController(ListingController):
|
||||
return self.redirect(promote.promo_edit_url(link))
|
||||
|
||||
@json_validate(sr=VSubmitSR('sr', promotion=True),
|
||||
location=VLocation(),
|
||||
start=VDate('startdate'),
|
||||
end=VDate('enddate'))
|
||||
def GET_check_inventory(self, responder, sr, start, end):
|
||||
def GET_check_inventory(self, responder, sr, location, start, end):
|
||||
sr = sr or Frontpage
|
||||
available_by_datestr = inventory.get_available_pageviews(sr, start, end,
|
||||
datestr=True)
|
||||
return {'inventory': available_by_datestr}
|
||||
if not location or not location.country:
|
||||
available = inventory.get_available_pageviews(sr, start, end,
|
||||
datestr=True)
|
||||
else:
|
||||
available = inventory.get_available_pageviews_geotargeted(sr,
|
||||
location, start, end, datestr=True)
|
||||
return {'inventory': available}
|
||||
|
||||
@validate(
|
||||
VSponsorAdmin(),
|
||||
@@ -564,9 +571,10 @@ class PromoteController(ListingController):
|
||||
sr=VSubmitSR('sr', promotion=True),
|
||||
campaign_id36=nop("campaign_id36"),
|
||||
targeting=VLength("targeting", 10),
|
||||
priority=VPriority("priority"))
|
||||
priority=VPriority("priority"),
|
||||
location=VLocation())
|
||||
def POST_edit_campaign(self, form, jquery, link, campaign_id36,
|
||||
dates, bid, sr, targeting, priority):
|
||||
dates, bid, sr, targeting, priority, location):
|
||||
if not link:
|
||||
return
|
||||
|
||||
@@ -574,6 +582,8 @@ class PromoteController(ListingController):
|
||||
|
||||
author = Account._byID(link.author_id, data=True)
|
||||
cpm = author.cpm_selfserve_pennies
|
||||
if location:
|
||||
cpm += g.cpm_selfserve_geotarget.pennies
|
||||
|
||||
if (start and end and not promote.is_accepted(link) and
|
||||
not c.user_is_sponsor):
|
||||
@@ -658,14 +668,18 @@ class PromoteController(ListingController):
|
||||
|
||||
# Check inventory
|
||||
campaign = campaign if campaign_id36 else None
|
||||
if (not priority.inventory_override and
|
||||
has_oversold_error(form, campaign, start, end, bid, cpm, sr)):
|
||||
return
|
||||
if not priority.inventory_override:
|
||||
oversold = has_oversold_error(form, campaign, start, end, bid, cpm,
|
||||
sr, location)
|
||||
if oversold:
|
||||
return
|
||||
|
||||
if campaign:
|
||||
promote.edit_campaign(link, campaign, dates, bid, cpm, sr, priority)
|
||||
promote.edit_campaign(link, campaign, dates, bid, cpm, sr, priority,
|
||||
location)
|
||||
else:
|
||||
campaign = promote.new_campaign(link, dates, bid, cpm, sr, priority)
|
||||
campaign = promote.new_campaign(link, dates, bid, cpm, sr, priority,
|
||||
location)
|
||||
rc = RenderableCampaign.from_campaigns(link, campaign)
|
||||
jquery.update_campaign(campaign._fullname, rc.render_html())
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ class Globals(object):
|
||||
'gold_month_price',
|
||||
'gold_year_price',
|
||||
'cpm_selfserve',
|
||||
'cpm_selfserve_geotarget',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -3571,6 +3571,39 @@ class PromoteLinkForm(Templated):
|
||||
self.priorities = [(p.name, p.text, p.description, p.default, p.inventory_override, p.cpm)
|
||||
for p in sorted(PROMOTE_PRIORITIES.values(), key=lambda p: p.value)]
|
||||
|
||||
# geotargeting
|
||||
def location_sort(location_tuple):
|
||||
code, name, default = location_tuple
|
||||
if code == '':
|
||||
return -2
|
||||
elif code == 'US':
|
||||
return -1
|
||||
else:
|
||||
return name
|
||||
|
||||
countries = [(code, country['name'], False) for code, country
|
||||
in g.locations.iteritems()]
|
||||
countries.append(('', _('none'), True))
|
||||
|
||||
self.countries = sorted(countries, key=location_sort)
|
||||
self.regions = {}
|
||||
self.metros = {}
|
||||
for code, country in g.locations.iteritems():
|
||||
if 'regions' in country and country['regions']:
|
||||
self.regions[code] = [('', _('all'), True)]
|
||||
|
||||
for region_code, region in country['regions'].iteritems():
|
||||
if region['metros']:
|
||||
region_tuple = (region_code, region['name'], False)
|
||||
self.regions[code].append(region_tuple)
|
||||
self.metros[region_code] = []
|
||||
|
||||
for metro_code, metro in region['metros'].iteritems():
|
||||
metro_tuple = (metro_code, metro['name'], False)
|
||||
self.metros[region_code].append(metro_tuple)
|
||||
self.metros[region_code].sort(key=location_sort)
|
||||
self.regions[code].sort(key=location_sort)
|
||||
|
||||
# preload some inventory
|
||||
srnames = set()
|
||||
for title, names in self.subreddit_selector.subreddit_names:
|
||||
@@ -3608,6 +3641,23 @@ class RenderableCampaign(Templated):
|
||||
self.pay_url = promote.pay_url(link, campaign)
|
||||
self.view_live_url = promote.view_live_url(link, campaign.sr_name)
|
||||
self.refund_url = promote.refund_url(link, campaign)
|
||||
|
||||
if campaign.location:
|
||||
country = campaign.location.country or ''
|
||||
region = campaign.location.region or ''
|
||||
metro = campaign.location.metro or ''
|
||||
pieces = [country, region]
|
||||
if metro:
|
||||
metro_str = (g.locations[country]['regions'][region]
|
||||
['metros'][metro]['name'])
|
||||
pieces.append(metro_str)
|
||||
pieces = filter(lambda i: i, pieces)
|
||||
self.geotarget = '/'.join(pieces)
|
||||
self.country, self.region, self.metro = country, region, metro
|
||||
else:
|
||||
self.geotarget = ''
|
||||
self.country, self.region, self.metro = '', '', ''
|
||||
|
||||
Templated.__init__(self)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -240,11 +240,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, sr, priority):
|
||||
def new_campaign(link, dates, bid, cpm, sr, priority, location):
|
||||
# empty string for sr_name means target to all
|
||||
sr_name = sr.name if sr else ""
|
||||
campaign = PromoCampaign._new(link, sr_name, bid, cpm, dates[0], dates[1],
|
||||
priority)
|
||||
priority, location)
|
||||
PromotionWeights.add(link, campaign._id, sr_name, dates[0], dates[1], bid)
|
||||
PromotionLog.add(link, 'campaign %s created' % campaign._id)
|
||||
|
||||
@@ -260,7 +260,7 @@ def new_campaign(link, dates, bid, cpm, sr, priority):
|
||||
def free_campaign(link, campaign, user):
|
||||
auth_campaign(link, campaign, user, -1)
|
||||
|
||||
def edit_campaign(link, campaign, dates, bid, cpm, sr, priority):
|
||||
def edit_campaign(link, campaign, dates, bid, cpm, sr, priority, location):
|
||||
sr_name = sr.name if sr else '' # empty string means target to all
|
||||
|
||||
changed = {}
|
||||
@@ -293,7 +293,7 @@ def edit_campaign(link, campaign, dates, bid, cpm, sr, priority):
|
||||
|
||||
# update values in the db
|
||||
campaign.update(dates[0], dates[1], bid, cpm, sr_name,
|
||||
campaign.trans_id, priority, commit=True)
|
||||
campaign.trans_id, priority, location, commit=True)
|
||||
|
||||
if campaign.priority.cpm:
|
||||
# make it a freebie, if applicable
|
||||
|
||||
@@ -4656,6 +4656,7 @@ ul.tabmenu.formtab {
|
||||
text-align: center;
|
||||
border: 1px solid #369;
|
||||
padding: 5px;
|
||||
max-width: 120px;
|
||||
}
|
||||
.existing-campaigns > table > tbody > tr#edit-campaign-tr > td {
|
||||
text-align: left;
|
||||
@@ -4736,6 +4737,12 @@ ul.tabmenu.formtab {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
#campaign .geotarget-select {
|
||||
float: left;
|
||||
clear: left;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/***traffic stuff***/
|
||||
.traffic-table,
|
||||
.traffic-tables-side fieldset {
|
||||
|
||||
@@ -13,6 +13,11 @@ r.sponsored = {
|
||||
}
|
||||
},
|
||||
|
||||
setup_geotargeting: function(regions, metros) {
|
||||
this.regions = regions
|
||||
this.metros = metros
|
||||
},
|
||||
|
||||
get_dates: function(startdate, enddate) {
|
||||
var start = $.datepicker.parseDate('mm/dd/yy', startdate),
|
||||
end = $.datepicker.parseDate('mm/dd/yy', enddate),
|
||||
@@ -27,11 +32,23 @@ r.sponsored = {
|
||||
return dates
|
||||
},
|
||||
|
||||
get_check_inventory: function(srname, dates) {
|
||||
get_inventory_key: function(srname, geotarget) {
|
||||
var inventoryKey = srname
|
||||
if (geotarget.country != "") {
|
||||
inventoryKey += "/" + geotarget.country
|
||||
}
|
||||
if (geotarget.metro != "") {
|
||||
inventoryKey += "/" + geotarget.metro
|
||||
}
|
||||
return inventoryKey
|
||||
},
|
||||
|
||||
get_check_inventory: function(srname, geotarget, dates) {
|
||||
var inventoryKey = this.get_inventory_key(srname, geotarget)
|
||||
var fetch = _.some(dates, function(date) {
|
||||
var datestr = $.datepicker.formatDate('mm/dd/yy', date)
|
||||
if (!(this.inventory[srname] && _.has(this.inventory[srname], datestr))) {
|
||||
r.debug('need to fetch ' + datestr + ' for ' + srname)
|
||||
if (!(this.inventory[inventoryKey] && _.has(this.inventory[inventoryKey], datestr))) {
|
||||
r.debug('need to fetch ' + datestr + ' for ' + inventoryKey)
|
||||
return true
|
||||
}
|
||||
}, this)
|
||||
@@ -46,17 +63,20 @@ r.sponsored = {
|
||||
url: '/api/check_inventory.json',
|
||||
data: {
|
||||
sr: srname,
|
||||
country: geotarget.country,
|
||||
region: geotarget.region,
|
||||
metro: geotarget.metro,
|
||||
startdate: $.datepicker.formatDate('mm/dd/yy', dates[0]),
|
||||
enddate: $.datepicker.formatDate('mm/dd/yy', end)
|
||||
},
|
||||
success: function(data) {
|
||||
if (!r.sponsored.inventory[srname]) {
|
||||
r.sponsored.inventory[srname] = {}
|
||||
if (!r.sponsored.inventory[inventoryKey]) {
|
||||
r.sponsored.inventory[inventoryKey] = {}
|
||||
}
|
||||
|
||||
for (var datestr in data.inventory) {
|
||||
if (!r.sponsored.inventory[srname][datestr]) {
|
||||
r.sponsored.inventory[srname][datestr] = data.inventory[datestr]
|
||||
if (!r.sponsored.inventory[inventoryKey][datestr]) {
|
||||
r.sponsored.inventory[inventoryKey][datestr] = data.inventory[datestr]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +86,7 @@ r.sponsored = {
|
||||
}
|
||||
},
|
||||
|
||||
get_booked_inventory: function($form, srname, isOverride) {
|
||||
get_booked_inventory: function($form, srname, geotarget, isOverride) {
|
||||
var campaign_name = $form.find('input[name="campaign_name"]').val()
|
||||
if (!campaign_name) {
|
||||
return {}
|
||||
@@ -86,6 +106,16 @@ r.sponsored = {
|
||||
return {}
|
||||
}
|
||||
|
||||
var existing_country = $campaign_row.data("country")
|
||||
if (geotarget.country != existing_country) {
|
||||
return {}
|
||||
}
|
||||
|
||||
var existing_metro = $campaign_row.data("metro")
|
||||
if (geotarget.metro != existing_metro) {
|
||||
return {}
|
||||
}
|
||||
|
||||
var existingOverride = $campaign_row.data("override")
|
||||
if (isOverride != existingOverride) {
|
||||
return {}
|
||||
@@ -120,8 +150,13 @@ r.sponsored = {
|
||||
targeted = $form.find('#targeting').is(':checked'),
|
||||
target = $form.find('*[name="sr"]').val(),
|
||||
srname = targeted ? target : '',
|
||||
country = $('#country').val() || "",
|
||||
region = $('#region').val() || "",
|
||||
metro = $('#metro').val() || "",
|
||||
geotarget = {'country': country, 'region': region, 'metro': metro},
|
||||
dates = r.sponsored.get_dates(startdate, enddate),
|
||||
booked = this.get_booked_inventory($form, srname, isOverride)
|
||||
booked = this.get_booked_inventory($form, srname, geotarget, isOverride),
|
||||
inventoryKey = this.get_inventory_key(srname, geotarget)
|
||||
|
||||
// bail out in state where targeting is selected but srname
|
||||
// has not been entered yet
|
||||
@@ -130,21 +165,21 @@ r.sponsored = {
|
||||
return
|
||||
}
|
||||
|
||||
$.when(r.sponsored.get_check_inventory(srname, dates)).done(
|
||||
$.when(r.sponsored.get_check_inventory(srname, geotarget, dates)).done(
|
||||
function() {
|
||||
if (isOverride) {
|
||||
// do a simple sum of available inventory for override
|
||||
var available = _.reduce(_.map(dates, function(date){
|
||||
var datestr = $.datepicker.formatDate('mm/dd/yy', date),
|
||||
daily_booked = booked[datestr] || 0
|
||||
return r.sponsored.inventory[srname][datestr] + daily_booked
|
||||
return r.sponsored.inventory[inventoryKey][datestr] + daily_booked
|
||||
}), function(memo, num){ return memo + num; }, 0)
|
||||
} else {
|
||||
// calculate conservative inventory estimate
|
||||
var minDaily = _.min(_.map(dates, function(date) {
|
||||
var datestr = $.datepicker.formatDate('mm/dd/yy', date),
|
||||
daily_booked = booked[datestr] || 0
|
||||
return r.sponsored.inventory[srname][datestr] + daily_booked
|
||||
return r.sponsored.inventory[inventoryKey][datestr] + daily_booked
|
||||
}))
|
||||
var available = minDaily * ndays
|
||||
}
|
||||
@@ -206,7 +241,15 @@ r.sponsored = {
|
||||
},
|
||||
|
||||
get_cpm: function($form) {
|
||||
return parseInt($form.find('*[name="cpm"]').val())
|
||||
var baseCpm = parseInt($("#bid").data("base_cpm")),
|
||||
geotargetCpm = parseInt($("#bid").data("geotarget_cpm")),
|
||||
isGeotarget = $('#country').val() != ''
|
||||
|
||||
if (isGeotarget) {
|
||||
return geotargetCpm
|
||||
} else {
|
||||
return baseCpm
|
||||
}
|
||||
},
|
||||
|
||||
on_date_change: function() {
|
||||
@@ -297,6 +340,59 @@ r.sponsored = {
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
update_regions: function() {
|
||||
var $country = $('#country'),
|
||||
$region = $('#region'),
|
||||
$metro = $('#metro')
|
||||
|
||||
$region.find('option').remove().end().hide()
|
||||
$metro.find('option').remove().end().hide()
|
||||
$metro.prop('disabled', true)
|
||||
|
||||
if (_.has(this.regions, $country.val())) {
|
||||
_.each(this.regions[$country.val()], function(item) {
|
||||
var code = item[0],
|
||||
name = item[1],
|
||||
selected = item[2]
|
||||
|
||||
$('<option/>', {value: code, selected: selected}).text(name).appendTo($region)
|
||||
})
|
||||
$region.show()
|
||||
}
|
||||
},
|
||||
|
||||
update_metros: function() {
|
||||
var $region = $('#region'),
|
||||
$metro = $('#metro')
|
||||
|
||||
$metro.find('option').remove().end().hide()
|
||||
if (_.has(this.metros, $region.val())) {
|
||||
_.each(this.metros[$region.val()], function(item) {
|
||||
var code = item[0],
|
||||
name = item[1],
|
||||
selected = item[2]
|
||||
|
||||
$('<option/>', {value: code, selected: selected}).text(name).appendTo($metro)
|
||||
})
|
||||
$metro.prop('disabled', false)
|
||||
$metro.show()
|
||||
}
|
||||
},
|
||||
|
||||
country_changed: function() {
|
||||
this.update_regions()
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
region_changed: function() {
|
||||
this.update_metros()
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
metro_changed: function() {
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
check_bid: function($form) {
|
||||
var bid = this.get_bid($form),
|
||||
minimum_bid = $("#bid").data("min_bid");
|
||||
@@ -497,6 +593,21 @@ function edit_campaign($campaign_row) {
|
||||
.find(".targeting").hide();
|
||||
}
|
||||
|
||||
/* set geotargeting */
|
||||
var country = $campaign_row.data("country"),
|
||||
region = $campaign_row.data("region"),
|
||||
metro = $campaign_row.data("metro")
|
||||
campaign.find("#country").val(country)
|
||||
r.sponsored.update_regions()
|
||||
if (region != "") {
|
||||
campaign.find("#region").val(region)
|
||||
r.sponsored.update_metros()
|
||||
|
||||
if (metro != "") {
|
||||
campaign.find("#metro").val(metro)
|
||||
}
|
||||
}
|
||||
|
||||
/* attach the dates to the date widgets */
|
||||
init_startdate();
|
||||
init_enddate();
|
||||
@@ -540,6 +651,9 @@ function create_campaign() {
|
||||
.find('input[name="priority"][data-default="true"]').prop("checked", "checked").end()
|
||||
.find('input[name="bid"]').val(minBid * 5).end()
|
||||
.find(".targeting").hide().end()
|
||||
.find('select[name="country"]').val('all').end()
|
||||
.find('select[name="region"]').hide().end()
|
||||
.find('select[name="metro"]').hide().end()
|
||||
.fadeIn();
|
||||
r.sponsored.fill_campaign_editor();
|
||||
});
|
||||
|
||||
@@ -43,8 +43,9 @@ ${unsafe(js.use('sponsored'))}
|
||||
<script type="text/javascript">
|
||||
r.sponsored.init();
|
||||
r.sponsored.setup(${unsafe(simplejson.dumps(thing.inventory))},
|
||||
${simplejson.dumps(not thing.campaigns)})
|
||||
|
||||
${simplejson.dumps(not thing.campaigns)});
|
||||
r.sponsored.setup_geotargeting(${unsafe(simplejson.dumps(thing.regions))},
|
||||
${unsafe(simplejson.dumps(thing.metros))});
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
@@ -336,6 +337,28 @@ ${self.javascript_setup()}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>${_("geotargeting")}</th>
|
||||
<td class="prefright">
|
||||
<select class="geotarget-select" id="country" name="country"
|
||||
title=${_("country")}
|
||||
onchange="r.sponsored.country_changed()">
|
||||
%for code, name, selected in thing.countries:
|
||||
<option ${"selected='selected'" if selected else ""} value=${code}>
|
||||
${name}
|
||||
</option>
|
||||
%endfor
|
||||
</select>
|
||||
<select class="geotarget-select" id="region" name="region"
|
||||
title=${_("region")} style="display:none"
|
||||
onchange="r.sponsored.region_changed()"></select>
|
||||
<select class="geotarget-select" id="metro" name="metro"
|
||||
title=${_("metro")} style="display:none"
|
||||
onchange="r.sponsored.metro_changed()"></select>
|
||||
${error_field("INVALID_LOCATION", ("country", "region", "metro"), "div")}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>${_("price")}</th>
|
||||
<td class="prefright">
|
||||
@@ -378,7 +401,8 @@ ${self.javascript_setup()}
|
||||
onkeyup="r.sponsored.on_bid_change()"
|
||||
value="${format_decimal(5 * thing.min_bid, format='.00', locale=c.locale)}"
|
||||
data-min_bid="${thing.min_bid}"
|
||||
data-base_cpm="${thing.author.cpm_selfserve_pennies}"/>
|
||||
data-base_cpm="${thing.author.cpm_selfserve_pennies}"
|
||||
data-geotarget_cpm="${thing.author.cpm_selfserve_pennies + g.cpm_selfserve_geotarget.pennies}"/>
|
||||
<div class="minimum-spend">
|
||||
${_('%(minimum)s minimum') % dict(minimum=format_currency(thing.min_bid, 'USD', locale=c.locale))}
|
||||
</div>
|
||||
@@ -486,6 +510,7 @@ ${self.javascript_setup()}
|
||||
<th>${_("total budget")}</th>
|
||||
<th>${_("spent")}</th>
|
||||
<th title="${targeting_title}">${_("targeting")}</th>
|
||||
<th>${_("geotargeting")}</th>
|
||||
<th style="align:right">
|
||||
<button class="new-campaign fancybutton"
|
||||
${'disabled="disabled"' if len(thing.campaigns) >= g.MAX_CAMPAIGNS_PER_LINK else ''}
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
data-enddate="${thing.campaign.end_date.strftime('%m/%d/%Y')}"
|
||||
data-bid="${'%.2f' % thing.campaign.bid}"
|
||||
data-targeting="${thing.campaign.sr_name}"
|
||||
data-country="${thing.country}"
|
||||
data-region="${thing.region}"
|
||||
data-metro="${thing.metro}"
|
||||
data-cpm="${getattr(thing.campaign, 'cpm', g.cpm_selfserve.pennies)}"
|
||||
data-campaign_id36="${thing.campaign._id36}"
|
||||
data-campaign_name="${thing.campaign._fullname}"
|
||||
@@ -107,6 +110,10 @@
|
||||
${'/r/%s' % thing.campaign.sr_name if thing.campaign.sr_name else _('frontpage')}
|
||||
</td>
|
||||
|
||||
<td class="campaign-geotarget">
|
||||
${thing.geotarget}
|
||||
</td>
|
||||
|
||||
<td class="campaign-buttons">
|
||||
%if thing.is_complete:
|
||||
<span class='info'>${_("complete")}</span>
|
||||
|
||||
Reference in New Issue
Block a user