Add geotargeting for selfserve advertising.

This commit is contained in:
Brian Simpson
2013-11-27 11:15:20 -05:00
parent 90cafd9933
commit 93f29d98f2
9 changed files with 253 additions and 34 deletions

View File

@@ -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)

View File

@@ -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())

View File

@@ -240,6 +240,7 @@ class Globals(object):
'gold_month_price',
'gold_year_price',
'cpm_selfserve',
'cpm_selfserve_geotarget',
],
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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();
});

View File

@@ -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 ''}

View File

@@ -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>