Permalink
Browse files

Add geotargeting for selfserve advertising.

  • Loading branch information...
1 parent 90cafd9 commit 93f29d98f23f135a3b3e5ac0bdbfc0106dbc82e4 @bsimpson63 bsimpson63 committed Nov 27, 2013
View
1 r2/example.ini
@@ -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
42 r2/r2/controllers/promotecontroller.py
@@ -75,6 +75,7 @@
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 @@ def GET_edit_promo_campaign(self, campaign):
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,16 +571,19 @@ def POST_rm_roadblock(self, form, jquery, dates, sr):
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
start, end = dates or (None, None)
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 @@ def POST_edit_campaign(self, form, jquery, link, campaign_id36,
# 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
1 r2/r2/lib/app_globals.py
@@ -240,6 +240,7 @@ class Globals(object):
'gold_month_price',
'gold_year_price',
'cpm_selfserve',
+ 'cpm_selfserve_geotarget',
],
}
View
50 r2/r2/lib/pages/pages.py
@@ -3571,6 +3571,39 @@ def setup(self, link, listing):
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 @@ def __init__(self, link, campaign, transaction, is_pending, is_live,
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
8 r2/r2/lib/promote.py
@@ -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
7 r2/r2/public/static/css/reddit.less
@@ -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
140 r2/r2/public/static/js/sponsored.js
@@ -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
31 r2/r2/templates/promotelinkform.html
@@ -43,8 +43,9 @@
<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>
@@ -337,6 +338,28 @@
</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">
<span class="price-info"></span>
@@ -378,7 +401,8 @@
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 @@
<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
7 r2/r2/templates/renderablecampaign.html
@@ -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>

0 comments on commit 93f29d9

Please sign in to comment.