mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-02-15 17:05:47 -05:00
Sell campaigns by CPM.
This commit is contained in:
committed by
Brian Simpson
parent
0d7736ac39
commit
a0c4a904e2
@@ -315,6 +315,7 @@ allowed_pay_countries = United States, United Kingdom, Canada
|
||||
sponsors =
|
||||
selfserve_support_email = selfservesupport@mydomain.com
|
||||
MAX_CAMPAIGNS_PER_LINK = 100
|
||||
cpm_selfserve = 1.00
|
||||
|
||||
# authorize.net credentials (blank authorizenetapi to disable)
|
||||
authorizenetapi =
|
||||
|
||||
@@ -330,7 +330,7 @@ def make_map():
|
||||
"freebie|promote_note|update_pay|refund|"
|
||||
"traffic_viewer|rm_traffic_viewer|"
|
||||
"edit_campaign|delete_campaign|meta_promo|"
|
||||
"add_roadblock|rm_roadblock")))
|
||||
"add_roadblock|rm_roadblock|check_inventory")))
|
||||
mc('/api/:action', controller='apiminimal',
|
||||
requirements=dict(action="new_captcha"))
|
||||
mc('/api/:type', controller='api',
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
###############################################################################
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from babel.numbers import format_number
|
||||
import itertools
|
||||
import json
|
||||
import urllib
|
||||
@@ -29,7 +30,7 @@ from pylons import c, g, request
|
||||
from pylons.i18n import _
|
||||
|
||||
from r2.controllers.listingcontroller import ListingController
|
||||
from r2.lib import cssfilter, promote
|
||||
from r2.lib import cssfilter, inventory, promote
|
||||
from r2.lib.authorize import get_account_info, edit_profile, PROFILE_LIMIT
|
||||
from r2.lib.db import queries
|
||||
from r2.lib.errors import errors
|
||||
@@ -52,8 +53,9 @@ from r2.lib.pages import (
|
||||
from r2.lib.pages.trafficpages import TrafficViewerList
|
||||
from r2.lib.pages.things import wrap_links
|
||||
from r2.lib.system_messages import user_added_messages
|
||||
from r2.lib.utils import make_offset_date
|
||||
from r2.lib.utils import make_offset_date, to_date
|
||||
from r2.lib.validator import (
|
||||
json_validate,
|
||||
nop,
|
||||
noresponse,
|
||||
VAccountByName,
|
||||
@@ -85,6 +87,7 @@ from r2.lib.validator import (
|
||||
VUrl,
|
||||
)
|
||||
from r2.models import (
|
||||
calc_impressions,
|
||||
Frontpage,
|
||||
Link,
|
||||
LiveAdWeights,
|
||||
@@ -248,6 +251,15 @@ class PromoteController(ListingController):
|
||||
link = Link._byID(campaign.link_id)
|
||||
return self.redirect(promote.promo_edit_url(link))
|
||||
|
||||
@json_validate(sr=VSubmitSR('sr', promotion=True),
|
||||
start=VDate('startdate'),
|
||||
end=VDate('enddate'))
|
||||
def GET_check_inventory(self, responder, sr, start, end):
|
||||
sr = sr or Frontpage
|
||||
available_by_datestr = inventory.get_available_pageviews(sr, start, end,
|
||||
datestr=True)
|
||||
return {'inventory': available_by_datestr}
|
||||
|
||||
@validate(VSponsor(),
|
||||
dates=VDateRange(["startdate", "enddate"],
|
||||
max_range=timedelta(days=28),
|
||||
@@ -454,6 +466,7 @@ class PromoteController(ListingController):
|
||||
return
|
||||
|
||||
start, end = dates or (None, None)
|
||||
cpm = g.cpm_selfserve.pennies
|
||||
|
||||
if (start and end and not promote.is_accepted(l) and
|
||||
not c.user_is_sponsor):
|
||||
@@ -487,20 +500,9 @@ class PromoteController(ListingController):
|
||||
form.has_errors('title', errors.TOO_MANY_CAMPAIGNS)
|
||||
return
|
||||
|
||||
duration = max((end - start).days, 1)
|
||||
|
||||
if form.has_errors('bid', errors.BAD_BID):
|
||||
return
|
||||
|
||||
# minimum bid depends on user privilege and targeting, checked here
|
||||
# instead of in the validator b/c current duration is needed
|
||||
if c.user_is_sponsor:
|
||||
min_daily_bid = 0
|
||||
elif targeting == 'one':
|
||||
min_daily_bid = g.min_promote_bid * 1.5
|
||||
else:
|
||||
min_daily_bid = g.min_promote_bid
|
||||
|
||||
if campaign_id36:
|
||||
# you cannot edit the bid of a live ad unless it's a freebie
|
||||
try:
|
||||
@@ -514,10 +516,11 @@ class PromoteController(ListingController):
|
||||
except NotFound:
|
||||
pass
|
||||
|
||||
if bid is None or bid / duration < min_daily_bid:
|
||||
min_bid = 0 if c.user_is_sponsor else g.min_promote_bid
|
||||
if bid is None or bid < min_bid:
|
||||
c.errors.add(errors.BAD_BID, field='bid',
|
||||
msg_params={'min': min_daily_bid,
|
||||
'max': g.max_promote_bid})
|
||||
msg_params={'min': min_bid,
|
||||
'max': g.max_promote_bid})
|
||||
form.has_errors('bid', errors.BAD_BID)
|
||||
return
|
||||
|
||||
@@ -539,17 +542,31 @@ class PromoteController(ListingController):
|
||||
if targeting == 'none':
|
||||
sr = None
|
||||
|
||||
# Check inventory
|
||||
ndays = (to_date(end) - to_date(start)).days
|
||||
total_request = calc_impressions(bid, cpm)
|
||||
daily_request = int(total_request / ndays)
|
||||
oversold = inventory.get_oversold(sr or Frontpage, start, end,
|
||||
daily_request)
|
||||
if oversold:
|
||||
msg_params = {'daily_request': format_number(daily_request,
|
||||
locale=c.locale)}
|
||||
c.errors.add(errors.OVERSOLD_DETAIL, field='bid',
|
||||
msg_params=msg_params)
|
||||
form.has_errors('bid', errors.OVERSOLD_DETAIL)
|
||||
return
|
||||
|
||||
if campaign_id36 is not None:
|
||||
campaign = PromoCampaign._byID36(campaign_id36)
|
||||
promote.edit_campaign(l, campaign, dates, bid, sr)
|
||||
promote.edit_campaign(l, campaign, dates, bid, cpm, sr)
|
||||
r = promote.get_renderable_campaigns(l, campaign)
|
||||
jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date,
|
||||
r.duration, r.bid, r.sr, r.status)
|
||||
r.duration, r.bid, r.cpm, r.sr, r.status)
|
||||
else:
|
||||
campaign = promote.new_campaign(l, dates, bid, sr)
|
||||
campaign = promote.new_campaign(l, dates, bid, cpm, sr)
|
||||
r = promote.get_renderable_campaigns(l, campaign)
|
||||
jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date,
|
||||
r.duration, r.bid, r.sr, r.status)
|
||||
r.duration, r.bid, r.cpm, r.sr, r.status)
|
||||
|
||||
@validatedForm(VSponsor('link_id'),
|
||||
VModhash(),
|
||||
|
||||
@@ -214,6 +214,7 @@ class Globals(object):
|
||||
config_gold_price: [
|
||||
'gold_month_price',
|
||||
'gold_year_price',
|
||||
'cpm_selfserve',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ error_list = dict((
|
||||
('INVALID_PREF', "that preference isn't valid"),
|
||||
('BAD_NUMBER', _("that number isn't in the right range (%(range)s)")),
|
||||
('BAD_STRING', _("you used a character here that we can't handle")),
|
||||
('BAD_BID', _("your bid must be at least $%(min)d per day and no more than to $%(max)d in total.")),
|
||||
('BAD_BID', _("your budget must be at least $%(min)d and no more than $%(max)d.")),
|
||||
('ALREADY_SUB', _("that link has already been submitted")),
|
||||
('SUBREDDIT_EXISTS', _('that subreddit already exists')),
|
||||
('SUBREDDIT_NOEXIST', _('that subreddit doesn\'t exist')),
|
||||
@@ -79,6 +79,7 @@ error_list = dict((
|
||||
('NO_EMAILS', _('please enter at least one email address')),
|
||||
('TOO_MANY_EMAILS', _('please only share to %(num)s emails at a time.')),
|
||||
('OVERSOLD', _('that subreddit has already been oversold on %(start)s to %(end)s. Please pick another subreddit or date.')),
|
||||
('OVERSOLD_DETAIL', _('We have insufficient inventory to fulfill your requested budget, target, and dates. Requested %(daily_request)s impressions per day.')),
|
||||
('BAD_DATE', _('please provide a date of the form mm/dd/yyyy')),
|
||||
('BAD_DATE_RANGE', _('the dates need to be in order and not identical')),
|
||||
('DATE_RANGE_TOO_LARGE', _('you must choose a date range of less than %(days)s days')),
|
||||
@@ -115,7 +116,7 @@ error_list = dict((
|
||||
('BAD_HASH', _("i don't believe you.")),
|
||||
('ALREADY_MODERATOR', _('that user is already a moderator')),
|
||||
('NO_INVITE_FOUND', _('there is no pending invite for that subreddit')),
|
||||
('BID_LIVE', _('you cannot edit the bid of a live ad')),
|
||||
('BID_LIVE', _('you cannot edit the budget of a live ad')),
|
||||
('TOO_MANY_CAMPAIGNS', _('you have too many campaigns for that promotion')),
|
||||
('BAD_JSONP_CALLBACK', _('that jsonp callback contains invalid characters')),
|
||||
('INVALID_PERMISSION_TYPE', _("permissions don't apply to that type of user")),
|
||||
|
||||
@@ -3339,6 +3339,8 @@ class PromoteLinkForm(Templated):
|
||||
pay_id=bid.pay_id,
|
||||
amount_str=format_currency(bid.bid, 'USD',
|
||||
locale=c.locale),
|
||||
charge_str=format_currency(bid.charge or bid.bid, 'USD',
|
||||
locale=c.locale),
|
||||
)
|
||||
self.bids.append(row)
|
||||
|
||||
@@ -3359,13 +3361,29 @@ class PromoteLinkForm(Templated):
|
||||
|
||||
self.mindate = mindate.strftime("%m/%d/%Y")
|
||||
|
||||
self.subreddit_selector = SubredditSelector()
|
||||
|
||||
# preload some inventory
|
||||
srnames = []
|
||||
for title, names in self.subreddit_selector.subreddit_names:
|
||||
srnames.extend(names)
|
||||
srs = Subreddit._by_name(srnames).values()
|
||||
srs.append(Frontpage)
|
||||
inv_start = startdate
|
||||
inv_end = startdate + datetime.timedelta(days=14)
|
||||
sr_inventory = inventory.get_available_pageviews(srs, inv_start,
|
||||
inv_end, datestr=True)
|
||||
sr_inventory[''] = sr_inventory[Frontpage.name]
|
||||
del sr_inventory[Frontpage.name]
|
||||
self.inventory = sr_inventory
|
||||
|
||||
self.link = promote.wrap_promoted(link)
|
||||
self.listing = listing
|
||||
campaigns = PromoCampaign._by_link(link._id)
|
||||
self.campaigns = promote.get_renderable_campaigns(link, campaigns)
|
||||
self.promotion_log = PromotionLog.get(link)
|
||||
|
||||
self.min_daily_bid = 0 if c.user_is_admin else g.min_promote_bid
|
||||
self.min_bid = 0 if c.user_is_sponsor else g.min_promote_bid
|
||||
|
||||
|
||||
class PromoAdminTool(Reddit):
|
||||
|
||||
@@ -65,6 +65,7 @@ from r2.models import (
|
||||
PromotionLog,
|
||||
PromotionWeights,
|
||||
Subreddit,
|
||||
traffic,
|
||||
)
|
||||
from r2.models.keyvalue import NamedGlobals
|
||||
|
||||
@@ -172,13 +173,14 @@ def campaign_is_live(link, campaign_index):
|
||||
# control functions
|
||||
|
||||
class RenderableCampaign():
|
||||
def __init__(self, campaign_id36, start_date, end_date, duration, bid, sr,
|
||||
status):
|
||||
def __init__(self, campaign_id36, start_date, end_date, duration, bid,
|
||||
cpm, sr, status):
|
||||
self.campaign_id36 = campaign_id36
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
self.duration = duration
|
||||
self.bid = bid
|
||||
self.cpm = cpm
|
||||
self.sr = sr
|
||||
self.status = status
|
||||
|
||||
@@ -197,6 +199,7 @@ class RenderableCampaign():
|
||||
duration = strings.time_label % dict(num=ndays,
|
||||
time=ungettext("day", "days", ndays))
|
||||
bid = "%.2f" % camp.bid
|
||||
cpm = getattr(camp, 'cpm', g.cpm_selfserve.pennies)
|
||||
sr = camp.sr_name
|
||||
status = {'paid': bool(transaction),
|
||||
'complete': False,
|
||||
@@ -212,8 +215,8 @@ class RenderableCampaign():
|
||||
elif transaction.is_charged() or transaction.is_refund():
|
||||
status['complete'] = True
|
||||
|
||||
rc = cls(campaign_id36, start_date, end_date, duration, bid, sr,
|
||||
status)
|
||||
rc = cls(campaign_id36, start_date, end_date, duration, bid,
|
||||
cpm, sr, status)
|
||||
r.append(rc)
|
||||
return r
|
||||
|
||||
@@ -320,10 +323,10 @@ 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, sr):
|
||||
def new_campaign(link, dates, bid, cpm, sr):
|
||||
# empty string for sr_name means target to all
|
||||
sr_name = sr.name if sr else ""
|
||||
campaign = PromoCampaign._new(link, sr_name, bid, dates[0], dates[1])
|
||||
campaign = PromoCampaign._new(link, sr_name, bid, cpm, dates[0], dates[1])
|
||||
PromotionWeights.add(link, campaign._id, sr_name, dates[0], dates[1], bid)
|
||||
PromotionLog.add(link, 'campaign %s created' % campaign._id)
|
||||
author = Account._byID(link.author_id, True)
|
||||
@@ -334,7 +337,7 @@ def new_campaign(link, dates, bid, sr):
|
||||
def free_campaign(link, campaign, user):
|
||||
auth_campaign(link, campaign, user, -1)
|
||||
|
||||
def edit_campaign(link, campaign, dates, bid, sr):
|
||||
def edit_campaign(link, campaign, dates, bid, cpm, sr):
|
||||
sr_name = sr.name if sr else '' # empty string means target to all
|
||||
try:
|
||||
# if the bid amount changed, cancel any pending transactions
|
||||
@@ -346,7 +349,8 @@ def edit_campaign(link, campaign, dates, bid, sr):
|
||||
dates[0], dates[1], bid)
|
||||
|
||||
# update values in the db
|
||||
campaign.update(dates[0], dates[1], bid, sr_name, campaign.trans_id, commit=True)
|
||||
campaign.update(dates[0], dates[1], bid, cpm, sr_name,
|
||||
campaign.trans_id, commit=True)
|
||||
|
||||
# record the transaction
|
||||
text = 'updated campaign %s. (bid: %0.2f)' % (campaign._id, bid)
|
||||
@@ -701,6 +705,8 @@ def make_daily_promotions(offset=0, test=False):
|
||||
else:
|
||||
print by_srid
|
||||
|
||||
finalize_completed_campaigns(daysago=offset+1)
|
||||
|
||||
# after launching as many campaigns as possible, raise an exception to
|
||||
# report any error campaigns. (useful for triggering alerts in irc)
|
||||
if error_campaigns:
|
||||
@@ -708,6 +714,66 @@ def make_daily_promotions(offset=0, test=False):
|
||||
"promotions: %r" % error_campaigns)
|
||||
|
||||
|
||||
def finalize_completed_campaigns(daysago=1):
|
||||
# PromoCampaign.end_date is utc datetime with year, month, day only
|
||||
now = datetime.now(g.tz)
|
||||
date = now - timedelta(days=daysago)
|
||||
date = date.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
q = PromoCampaign._query(PromoCampaign.c.end_date == date,
|
||||
# exclude no transaction and freebies
|
||||
PromoCampaign.c.trans_id > 0,
|
||||
data=True)
|
||||
campaigns = list(q)
|
||||
|
||||
if not campaigns:
|
||||
return
|
||||
|
||||
# check that traffic is up to date
|
||||
earliest_campaign = min(campaigns, key=lambda camp: camp.start_date)
|
||||
start, end = get_total_run(earliest_campaign)
|
||||
missing_traffic = traffic.get_missing_traffic(start.replace(tzinfo=None),
|
||||
date.replace(tzinfo=None))
|
||||
if missing_traffic:
|
||||
raise ValueError("Can't finalize campaigns finished on %s."
|
||||
"Missing traffic from %s" % (date, missing_traffic))
|
||||
|
||||
links = Link._byID([camp.link_id for link in links], data=True)
|
||||
|
||||
for camp in campaigns:
|
||||
if hasattr(camp, 'refund_amount'):
|
||||
continue
|
||||
|
||||
link = links[camp.link_id]
|
||||
billable_impressions = get_billable_impressions(camp)
|
||||
billable_amount = get_billable_amount(camp, billable_impressions)
|
||||
|
||||
if billable_amount >= camp.bid:
|
||||
text = ('%s completed with $%s billable (%s impressions @ $%s).'
|
||||
% (camp, billable_amount, billable_impressions, camp.cpm))
|
||||
PromotionLog.add(link, text)
|
||||
refund_amount = 0.
|
||||
else:
|
||||
refund_amount = camp.bid - billable_amount
|
||||
user = Account._byID(link.author_id, data=True)
|
||||
try:
|
||||
success = authorize.refund_transaction(user, camp.trans_id,
|
||||
camp._id, refund_amount)
|
||||
except authorize.AuthorizeNetException as e:
|
||||
text = ('%s $%s refund failed' % (camp, refund_amount))
|
||||
PromotionLog.add(link, text)
|
||||
g.log.debug(text + ' (response: %s)' % e)
|
||||
continue
|
||||
text = ('%s completed with $%s billable (%s impressions @ $%s).'
|
||||
' %s refunded.' % (camp, billable_amount,
|
||||
billable_impressions, camp.cpm,
|
||||
refund_amount))
|
||||
PromotionLog.add(link, text)
|
||||
|
||||
camp.refund_amount = refund_amount
|
||||
camp._commit()
|
||||
|
||||
|
||||
PromoTuple = namedtuple('PromoTuple', ['link', 'weight', 'campaign'])
|
||||
|
||||
|
||||
@@ -809,6 +875,41 @@ def get_traffic_dates(thing):
|
||||
return start, end
|
||||
|
||||
|
||||
def get_billable_impressions(campaign):
|
||||
start, end = get_traffic_dates(campaign)
|
||||
if start > datetime.now(g.tz):
|
||||
return 0
|
||||
|
||||
traffic_lookup = traffic.TargetedImpressionsByCodename.promotion_history
|
||||
imps = traffic_lookup(campaign._fullname, start.replace(tzinfo=None),
|
||||
end.replace(tzinfo=None))
|
||||
billable_impressions = sum(imp for date, (imp,) in imps)
|
||||
return billable_impressions
|
||||
|
||||
|
||||
def get_billable_amount(camp, impressions):
|
||||
if hasattr(camp, 'cpm'):
|
||||
value_delivered = impressions / 1000. * camp.cpm / 100.
|
||||
billable_amount = min(camp.bid, value_delivered)
|
||||
else:
|
||||
# pre-CPM campaigns are charged in full regardless of impressions
|
||||
billable_amount = camp.bid
|
||||
return billable_amount
|
||||
|
||||
|
||||
def get_spent_amount(campaign):
|
||||
if hasattr(campaign, 'refund_amount'):
|
||||
# no need to calculate spend if we've already refunded
|
||||
spent = campaign.bid - campaign.refund_amount
|
||||
elif not hasattr(campaign, 'cpm'):
|
||||
# pre-CPM campaign
|
||||
return campaign.bid
|
||||
else:
|
||||
billable_impressions = get_billable_impressions(campaign)
|
||||
spent = get_billable_amount(campaign, billable_impressions)
|
||||
return spent
|
||||
|
||||
|
||||
def Run(offset=0, verbose=True):
|
||||
"""reddit-job-update_promos: Intended to be run hourly to pull in
|
||||
scheduled changes to ads
|
||||
|
||||
@@ -55,6 +55,12 @@ def get_promote_srid(name = 'promos'):
|
||||
return sr._id
|
||||
|
||||
|
||||
def calc_impressions(bid, cpm_pennies):
|
||||
# bid is in dollars, cpm_pennies is pennies
|
||||
# CPM is cost per 1000 impressions
|
||||
return int(bid / cpm_pennies * 1000 * 100)
|
||||
|
||||
|
||||
NO_TRANSACTION = 0
|
||||
|
||||
class PromoCampaign(Thing):
|
||||
@@ -67,10 +73,11 @@ class PromoCampaign(Thing):
|
||||
return val
|
||||
|
||||
@classmethod
|
||||
def _new(cls, link, sr_name, bid, start_date, end_date):
|
||||
def _new(cls, link, sr_name, bid, cpm, start_date, end_date):
|
||||
pc = PromoCampaign(link_id=link._id,
|
||||
sr_name=sr_name,
|
||||
bid=bid,
|
||||
cpm=cpm,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
trans_id=NO_TRANSACTION,
|
||||
@@ -99,6 +106,13 @@ class PromoCampaign(Thing):
|
||||
def ndays(self):
|
||||
return (self.end_date - self.start_date).days
|
||||
|
||||
@property
|
||||
def impressions(self):
|
||||
# deal with pre-CPM PromoCampaigns
|
||||
if not hasattr(self, 'cpm'):
|
||||
return -1
|
||||
return calc_impressions(self.bid, self.cpm)
|
||||
|
||||
def is_freebie(self):
|
||||
return self.trans_id < 0
|
||||
|
||||
@@ -106,10 +120,12 @@ class PromoCampaign(Thing):
|
||||
now = datetime.now(g.tz)
|
||||
return self.start_date < now and self.end_date > now
|
||||
|
||||
def update(self, start_date, end_date, bid, sr_name, trans_id, commit=True):
|
||||
def update(self, start_date, end_date, bid, cpm, sr_name, trans_id,
|
||||
commit=True):
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
self.bid = bid
|
||||
self.cpm = cpm
|
||||
self.sr_name = sr_name
|
||||
self.trans_id = trans_id
|
||||
if commit:
|
||||
|
||||
@@ -4303,12 +4303,15 @@ ul.tabmenu.formtab {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.targeting > ul {
|
||||
.campaign .notes ul {
|
||||
font-size: x-small;
|
||||
list-style-type: disc;
|
||||
margin: 0 20px 10px;
|
||||
}
|
||||
.existing-campaigns td > button { margin: 0px 5px 0px 0px; }
|
||||
.existing-campaigns td > button {
|
||||
margin: 2px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.campaign .bid-info { font-size: x-small; }
|
||||
.campaign .bid-info.error { color: red; }
|
||||
@@ -4319,7 +4322,7 @@ ul.tabmenu.formtab {
|
||||
.campaign #bid {
|
||||
text-align: right;
|
||||
}
|
||||
.campaign .targeting {
|
||||
.campaign .targeting, .campaign .notes {
|
||||
margin-left: 25px;
|
||||
}
|
||||
.campaign .targeting input{
|
||||
@@ -4346,6 +4349,13 @@ ul.tabmenu.formtab {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#campaign td,
|
||||
#campaign span,
|
||||
#campaign label,
|
||||
#campaign li {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
/***traffic stuff***/
|
||||
.traffic-table,
|
||||
.traffic-tables-side fieldset {
|
||||
@@ -4961,6 +4971,10 @@ table.lined-table {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div #campaign-field {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.create-promotion .help {
|
||||
font-size: x-small;
|
||||
}
|
||||
@@ -4977,7 +4991,12 @@ table.lined-table {
|
||||
}
|
||||
|
||||
|
||||
.create-promo { float: left; width: 520px; margin-right: 20px;}
|
||||
.create-promo {
|
||||
float: left;
|
||||
width: 570px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.create-promo .infobar {
|
||||
margin-right: 0;
|
||||
border-color: red;
|
||||
@@ -4992,6 +5011,11 @@ table.lined-table {
|
||||
}
|
||||
.create-promo .rules { float: left; margin-left: 15px; }
|
||||
|
||||
.create-promo textarea,
|
||||
.create-promo input[type=text] {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.fancy-settings h1, .create-promotion h1 { font-size: 200%; color: #999; margin:10px 5px; }
|
||||
.fancy-settings h2 { font-size: 200%; font-weight:normal; color: #999; margin:10px 5px; }
|
||||
.fancy-settings h1 strong { font-weight:bold; color: #666; }
|
||||
|
||||
@@ -2,6 +2,199 @@ function update_box(elem) {
|
||||
$(elem).prevAll('*[type="checkbox"]:first').prop('checked', true);
|
||||
};
|
||||
|
||||
r.sponsored = {
|
||||
init: function() {
|
||||
$("#sr-autocomplete").on("sr-changed blur", function() {
|
||||
r.sponsored.fill_campaign_editor()
|
||||
})
|
||||
|
||||
this.inventory = {}
|
||||
},
|
||||
|
||||
setup: function(inventory_by_sr) {
|
||||
this.inventory = inventory_by_sr
|
||||
},
|
||||
|
||||
get_dates: function(startdate, enddate) {
|
||||
var start = $.datepicker.parseDate('mm/dd/yy', startdate),
|
||||
end = $.datepicker.parseDate('mm/dd/yy', enddate),
|
||||
ndays = (end - start) / (1000 * 60 * 60 * 24),
|
||||
dates = []
|
||||
|
||||
for (var i=0; i < ndays; i++) {
|
||||
var d = new Date(start.getTime())
|
||||
d.setDate(start.getDate() + i)
|
||||
dates.push(d)
|
||||
}
|
||||
return dates
|
||||
},
|
||||
|
||||
get_check_inventory: function(srname, dates) {
|
||||
var fetch = _.some(dates, function(date) {
|
||||
var datestr = $.datepicker.formatDate('mm/dd/yy', date)
|
||||
if (!(this.inventory[srname] && this.inventory[srname][datestr])) {
|
||||
r.debug('need to fetch ' + datestr + ' for ' + srname)
|
||||
return true
|
||||
}
|
||||
}, this)
|
||||
|
||||
if (fetch) {
|
||||
dates.sort(function(d1,d2){return d1 - d2})
|
||||
var end = new Date(dates[dates.length-1].getTime())
|
||||
end.setDate(end.getDate() + 5)
|
||||
|
||||
return $.ajax({
|
||||
type: 'GET',
|
||||
url: '/api/check_inventory.json',
|
||||
data: {
|
||||
sr: srname,
|
||||
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] = {}
|
||||
}
|
||||
|
||||
for (var datestr in data.inventory) {
|
||||
r.sponsored.inventory[srname][datestr] = data.inventory[datestr]
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
},
|
||||
|
||||
check_inventory: function($form) {
|
||||
var bid = this.get_bid($form),
|
||||
cpm = this.get_cpm($form),
|
||||
requested = this.calc_impressions(bid, cpm),
|
||||
startdate = $form.find('*[name="startdate"]').val(),
|
||||
enddate = $form.find('*[name="enddate"]').val(),
|
||||
ndays = this.get_duration($form),
|
||||
daily_request = Math.floor(requested / ndays),
|
||||
targeted = $form.find('#targeting').is(':checked'),
|
||||
target = $form.find('*[name="sr"]').val(),
|
||||
srname = targeted ? target : '',
|
||||
dates = r.sponsored.get_dates(startdate, enddate)
|
||||
|
||||
$.when(r.sponsored.get_check_inventory(srname, dates)).done(
|
||||
function() {
|
||||
var oversold = {}
|
||||
|
||||
_.each(dates, function(date) {
|
||||
var datestr = $.datepicker.formatDate('mm/dd/yy', date),
|
||||
available = r.sponsored.inventory[srname][datestr]
|
||||
if (available < daily_request) {
|
||||
oversold[datestr] = available
|
||||
}
|
||||
})
|
||||
|
||||
if (!_.isEmpty(oversold)) {
|
||||
var oversold_dates = _.keys(oversold)
|
||||
|
||||
var message = r._("We have insufficient inventory to fulfill" +
|
||||
" your requested budget, target, and dates." +
|
||||
" Requested %(daily_request)s impressions " +
|
||||
"per day."
|
||||
).format({daily_request: r.utils.prettyNumber(daily_request)})
|
||||
|
||||
$(".OVERSOLD_DETAIL").text(message).show()
|
||||
var available_list = $('<ul>').appendTo(".OVERSOLD_DETAIL")
|
||||
_.each(oversold, function(num, datestr) {
|
||||
var available_msg = r._("%(num)s available on %(date)s").format({
|
||||
num: r.utils.prettyNumber(num),
|
||||
date: datestr
|
||||
})
|
||||
available_list.append($('<li>').text(available_msg))
|
||||
})
|
||||
|
||||
r.sponsored.disable_form($form)
|
||||
} else {
|
||||
$(".OVERSOLD_DETAIL").hide()
|
||||
r.sponsored.enable_form($form)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
get_duration: function($form) {
|
||||
return Math.round((Date.parse($form.find('*[name="enddate"]').val()) -
|
||||
Date.parse($form.find('*[name="startdate"]').val())) / (86400*1000))
|
||||
},
|
||||
|
||||
get_bid: function($form) {
|
||||
return parseFloat($form.find('*[name="bid"]').val())
|
||||
},
|
||||
|
||||
get_cpm: function($form) {
|
||||
return parseInt($form.find('*[name="cpm"]').val())
|
||||
},
|
||||
|
||||
on_date_change: function() {
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
on_bid_change: function() {
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
fill_campaign_editor: function() {
|
||||
var $form = $("#campaign"),
|
||||
bid = this.get_bid($form),
|
||||
cpm = this.get_cpm($form),
|
||||
ndays = this.get_duration($form),
|
||||
impressions = this.calc_impressions(bid, cpm);
|
||||
|
||||
$(".duration").text(ndays + " " + ((ndays > 1) ? r._("days") : r._("day")))
|
||||
$(".impression-info").text(r._("%(num)s impressions").format({num: r.utils.prettyNumber(impressions)}))
|
||||
$(".price-info").text(r._("$%(cpm)s per 1,000 impressions").format({cpm: (cpm/100).toFixed(2)}))
|
||||
|
||||
this.check_bid($form)
|
||||
this.check_inventory($form)
|
||||
},
|
||||
|
||||
disable_form: function($form) {
|
||||
$form.find('button[name="create"], button[name="save"]')
|
||||
.prop("disabled", "disabled")
|
||||
.addClass("disabled");
|
||||
},
|
||||
|
||||
enable_form: function($form) {
|
||||
$form.find('button[name="create"], button[name="save"]')
|
||||
.removeProp("disabled")
|
||||
.removeClass("disabled");
|
||||
},
|
||||
|
||||
targeting_on: function() {
|
||||
$('.targeting').find('*[name="sr"]').prop("disabled", "").end().slideDown();
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
targeting_off: function() {
|
||||
$('.targeting').find('*[name="sr"]').prop("disabled", "disabled").end().slideUp();
|
||||
this.fill_campaign_editor()
|
||||
},
|
||||
|
||||
check_bid: function($form) {
|
||||
var bid = this.get_bid($form),
|
||||
minimum_bid = $("#bid").data("min_bid");
|
||||
|
||||
$(".minimum-spend").removeClass("error");
|
||||
if (bid < minimum_bid) {
|
||||
$(".minimum-spend").addClass("error");
|
||||
this.disable_form($form)
|
||||
} else {
|
||||
this.enable_form($form)
|
||||
}
|
||||
},
|
||||
|
||||
calc_impressions: function(bid, cpm_pennies) {
|
||||
return bid / cpm_pennies * 1000 * 100
|
||||
}
|
||||
}
|
||||
|
||||
function update_bid(elem) {
|
||||
var form = $(elem).parents(".campaign");
|
||||
var is_targeted = $("#targeting").prop("checked");
|
||||
@@ -98,19 +291,6 @@ function check_enddate(startdate, enddate) {
|
||||
$("#datepicker-" + enddate.attr("id")).datepicker("destroy");
|
||||
}
|
||||
|
||||
function targeting_on(elem) {
|
||||
$(elem).parents(".campaign").find(".targeting")
|
||||
.find('*[name="sr"]').prop("disabled", "").end().slideDown();
|
||||
|
||||
update_bid(elem);
|
||||
}
|
||||
|
||||
function targeting_off(elem) {
|
||||
$(elem).parents(".campaign").find(".targeting")
|
||||
.find('*[name="sr"]').prop("disabled", "disabled").end().slideUp();
|
||||
|
||||
update_bid(elem);
|
||||
}
|
||||
|
||||
(function($) {
|
||||
|
||||
@@ -134,14 +314,15 @@ function get_flag_class(flags) {
|
||||
return css_class
|
||||
}
|
||||
|
||||
$.new_campaign = function(campaign_id36, start_date, end_date, duration,
|
||||
bid, targeting, flags) {
|
||||
$.new_campaign = function(campaign_id36, start_date, end_date, duration,
|
||||
bid, cpm, targeting, flags) {
|
||||
cancel_edit(function() {
|
||||
var data =('<input type="hidden" name="startdate" value="' +
|
||||
start_date +'"/>' +
|
||||
'<input type="hidden" name="enddate" value="' +
|
||||
end_date + '"/>' +
|
||||
'<input type="hidden" name="bid" value="' + bid + '"/>' +
|
||||
'<input type="hidden" name="cpm" value="' + cpm + '"/>' +
|
||||
'<input type="hidden" name="targeting" value="' +
|
||||
(targeting || '') + '"/>' +
|
||||
'<input type="hidden" name="campaign_id36" value="' + campaign_id36 + '"/>');
|
||||
@@ -165,8 +346,8 @@ $.new_campaign = function(campaign_id36, start_date, end_date, duration,
|
||||
return $;
|
||||
};
|
||||
|
||||
$.update_campaign = function(campaign_id36, start_date, end_date,
|
||||
duration, bid, targeting, flags) {
|
||||
$.update_campaign = function(campaign_id36, start_date, end_date,
|
||||
duration, bid, cpm, targeting, flags) {
|
||||
cancel_edit(function() {
|
||||
$('.existing-campaigns input[name="campaign_id36"]')
|
||||
.filter('*[value="' + (campaign_id36 || '0') + '"]')
|
||||
@@ -182,6 +363,7 @@ $.update_campaign = function(campaign_id36, start_date, end_date,
|
||||
.find('*[name="enddate"]').val(end_date).end()
|
||||
.find('*[name="targeting"]').val(targeting).end()
|
||||
.find('*[name="bid"]').val(bid).end()
|
||||
.find('*[name="cpm"]').val(cpm).end()
|
||||
.find("button, span").remove();
|
||||
$.set_up_campaigns();
|
||||
});
|
||||
@@ -199,10 +381,9 @@ $.set_up_campaigns = function() {
|
||||
var td = $(this).find("td:last");
|
||||
var bid_td = $(this).find("td:first").next().next().next()
|
||||
.addClass("bid");
|
||||
var target_td = $(this).find("td:nth-child(5)")
|
||||
if(td.length && ! td.children("button, span").length ) {
|
||||
if(tr.hasClass("live")) {
|
||||
$(target_td).append($(view).addClass("view")
|
||||
$(td).append($(view).addClass("view fancybutton")
|
||||
.click(function() { view_campaign(tr) }));
|
||||
}
|
||||
/* once paid, we shouldn't muck around with the campaign */
|
||||
@@ -313,11 +494,11 @@ function edit_campaign(elem) {
|
||||
"css_class": "", "cells": [""]}],
|
||||
tr.rowIndex + 1);
|
||||
$("#edit-campaign-tr").children('td:first')
|
||||
.attr("colspan", 6).append(campaign).end()
|
||||
.attr("colspan", 7).append(campaign).end()
|
||||
.prev().fadeOut(function() {
|
||||
var data_tr = $(this);
|
||||
var c = $("#campaign");
|
||||
$.map(['startdate', 'enddate', 'bid', 'campaign_id36'],
|
||||
$.map(['startdate', 'enddate', 'bid', 'cpm', 'campaign_id36'],
|
||||
function(i) {
|
||||
i = '*[name="' + i + '"]';
|
||||
c.find(i).val(data_tr.find(i).val());
|
||||
@@ -343,7 +524,7 @@ function edit_campaign(elem) {
|
||||
init_enddate();
|
||||
c.find('button[name="save"]').show().end()
|
||||
.find('button[name="create"]').hide().end();
|
||||
update_bid('*[name="bid"]');
|
||||
r.sponsored.fill_campaign_editor();
|
||||
c.fadeIn();
|
||||
} );
|
||||
}
|
||||
@@ -363,11 +544,12 @@ function check_number_of_campaigns(){
|
||||
}
|
||||
}
|
||||
|
||||
function create_campaign(elem) {
|
||||
function create_campaign() {
|
||||
if (check_number_of_campaigns()){
|
||||
return;
|
||||
}
|
||||
cancel_edit(function() {;
|
||||
var base_cpm = $("#bid").data("base_cpm")
|
||||
init_startdate();
|
||||
init_enddate();
|
||||
$("#campaign")
|
||||
@@ -379,8 +561,9 @@ function create_campaign(elem) {
|
||||
.prop("checked", "checked").end()
|
||||
.find(".targeting").hide().end()
|
||||
.find('*[name="sr"]').val("").prop("disabled", "disabled").end()
|
||||
.find('input[name="cpm"]').val(base_cpm).end()
|
||||
.fadeIn();
|
||||
update_bid('*[name="bid"]');
|
||||
r.sponsored.fill_campaign_editor();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,18 @@ r.utils = {
|
||||
return _.escape(str).replace(this._mdLinkRe, function(match, text, url) {
|
||||
return '<a href="' + url + '">' + text + '</a>'
|
||||
})
|
||||
},
|
||||
|
||||
prettyNumber: function(number) {
|
||||
// Add commas to separate every third digit
|
||||
var numberAsInt = parseInt(number)
|
||||
if (numberAsInt) {
|
||||
return numberAsInt.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Nothing is true. Everything is permitted.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
###############################################################################
|
||||
|
||||
<%!
|
||||
from babel.numbers import format_currency
|
||||
from r2.lib.template_helpers import static
|
||||
from r2.lib import js
|
||||
%>
|
||||
@@ -43,11 +44,8 @@ ${unsafe(js.use('sponsored'))}
|
||||
<p id="bid-field">
|
||||
<input type="hidden" name="campaign" value="${thing.campaign.campaign_id36}" />
|
||||
<input type="hidden" name="link" value="${thing.link._fullname}" />
|
||||
${unsafe(_("Your current bid is $%(bid)s") % dict(bid=thing.campaign.bid))}
|
||||
${error_field("BAD_BID", "bid")}
|
||||
<span class="gray">
|
||||
 ${_('(total for the duration provided)')}
|
||||
</span>
|
||||
<% budget=format_currency(float(thing.campaign.bid), 'USD', locale=c.locale) %>
|
||||
${unsafe(_("Your current budget is %(budget)s") % dict(budget=budget))}
|
||||
</p>
|
||||
%if thing.profiles:
|
||||
<p>
|
||||
@@ -72,7 +70,8 @@ ${unsafe(js.use('sponsored'))}
|
||||
</p>
|
||||
%endif
|
||||
<p class="info">
|
||||
${_("NOTE: your card will not be charged until the link has been queued for promotion.")}
|
||||
${_("NOTE: your card will not be charged until the campaign has been queued "
|
||||
"for promotion.")}
|
||||
</p>
|
||||
<input type="hidden" name="customer_id" value="${thing.customer_id}" />
|
||||
|
||||
|
||||
@@ -25,11 +25,12 @@
|
||||
from r2.lib.media import thumbnail_url
|
||||
from r2.lib.template_helpers import static
|
||||
from r2.lib import promote
|
||||
from r2.lib.pages import SubredditSelector
|
||||
from r2.lib.strings import strings
|
||||
from r2.models import Account
|
||||
from r2.lib import js
|
||||
import simplejson
|
||||
|
||||
from babel.numbers import format_currency, format_decimal
|
||||
%>
|
||||
|
||||
<%namespace file="utils.html"
|
||||
@@ -39,13 +40,12 @@
|
||||
${unsafe(js.use('sponsored'))}
|
||||
|
||||
<%def name="javascript_setup()">
|
||||
<script type="text/javascript">
|
||||
$(function() { update_bid("*[name=bid]"); });
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
r.sponsored.init();
|
||||
r.sponsored.setup(${unsafe(simplejson.dumps(thing.inventory))})
|
||||
</script>
|
||||
</%def>
|
||||
|
||||
${self.javascript_setup()}
|
||||
|
||||
## Create a datepicker for a form. min/maxDateSrc are the id of the
|
||||
## element containing the min/max date - the '#' is added automatically
|
||||
## here (as a workaround for babel message extraction not handling it
|
||||
@@ -109,6 +109,8 @@ ${self.javascript_setup()}
|
||||
${self.right_panel()}
|
||||
</div>
|
||||
|
||||
${self.javascript_setup()}
|
||||
|
||||
<%def name="title_field(link, editable=False)">
|
||||
<%utils:line_field title="${_('title')}" id="title-field" css_class="rounded">
|
||||
<textarea name="title" rows="2" cols="1"
|
||||
@@ -266,7 +268,7 @@ ${self.javascript_setup()}
|
||||
|
||||
<table class="preftable">
|
||||
<tr>
|
||||
<th>duration</th>
|
||||
<th>dates</th>
|
||||
<td class="prefright">
|
||||
<%
|
||||
mindate = thing.startdate
|
||||
@@ -278,14 +280,14 @@ ${self.javascript_setup()}
|
||||
minDateSrc="date-min" initfuncname="init_startdate">
|
||||
function(elem) {
|
||||
check_enddate(elem, $("#enddate"));
|
||||
update_bid(elem);
|
||||
r.sponsored.on_date_change();
|
||||
}
|
||||
</%self:datepicker>
|
||||
-
|
||||
<%self:datepicker name="enddate", value="${thing.enddate}"
|
||||
minDateSrc="startdate" initfuncname="init_enddate"
|
||||
min_date_offset="86400000">
|
||||
function(elem) { update_bid(elem); }
|
||||
function(elem) { r.sponsored.on_date_change(); }
|
||||
</%self:datepicker>
|
||||
|
||||
${error_field("BAD_DATE", "startdate", "div")}
|
||||
@@ -297,44 +299,72 @@ ${self.javascript_setup()}
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>total bid</th>
|
||||
<th>duration</th>
|
||||
<td class="prefright duration">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>total budget</th>
|
||||
<td class="prefright">
|
||||
${error_field("BAD_BID", "bid", "div")}
|
||||
${error_field("BID_LIVE", "bid", "div")}
|
||||
${error_field("OVERSOLD_DETAIL", "bid", "div")}
|
||||
$<input id="bid" name="bid" size="7" type="text"
|
||||
class="rounded styled-input"
|
||||
style="width:auto"
|
||||
onchange="update_bid(this)"
|
||||
onkeyup="update_bid(this)"
|
||||
title="Minimum is $${'%.2f' % thing.min_daily_bid} per day, or $${'%.2f' % (thing.min_daily_bid * 1.5)} per day targeted"
|
||||
value="${'%.2f' % (thing.min_daily_bid * 5)}"
|
||||
data-min_daily_bid="${thing.min_daily_bid}"/>
|
||||
<span class="bid-info gray"></span>
|
||||
onchange="r.sponsored.on_bid_change()"
|
||||
onkeyup="r.sponsored.on_bid_change()"
|
||||
title="Minimum is ${format_currency(thing.min_bid, 'USD', locale=c.locale)}"
|
||||
value="${format_decimal(5 * thing.min_bid, format='.00', locale=c.locale)}"
|
||||
data-min_bid="${thing.min_bid}"
|
||||
data-base_cpm="${g.cpm_selfserve.pennies}"/>
|
||||
<div class="minimum-spend">
|
||||
${_("$%.2F minimum") % thing.min_bid}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>price</th>
|
||||
<td class="prefright">
|
||||
<input id="cpm" name="cpm" value="${g.cpm_selfserve.pennies}" type="hidden">
|
||||
<span class="price-info"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>impressions</th>
|
||||
<td class="prefright">
|
||||
<span class="impression-info"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>targeting</th>
|
||||
<td class="prefright">
|
||||
<input id="no_targeting" class="nomargin"
|
||||
type="radio" value="none" name="targeting"
|
||||
onclick="return targeting_off(this)"
|
||||
checked="checked" />
|
||||
<label for="no_targeting">no targeting (displays site-wide)</label>
|
||||
<p id="no_targeting_minimum" class="minimum-spend">minimum $20 / day</p>
|
||||
<input id="targeting" class="nomargin"
|
||||
type="radio" value="one" name="targeting"
|
||||
onclick="return targeting_on(this)" />
|
||||
<label for="targeting">enable targeting (runs on a specific subreddit)</label>
|
||||
<p id="targeted_minimum" class="minimum-spend">minimum $30 / day</p>
|
||||
<label>
|
||||
<input id="no_targeting" class="nomargin"
|
||||
type="radio" value="none" name="targeting"
|
||||
onclick="r.sponsored.targeting_off()"
|
||||
checked="checked" />
|
||||
no targeting (displays site-wide)
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
<input id="targeting" class="nomargin"
|
||||
type="radio" value="one" name="targeting"
|
||||
onclick="r.sponsored.targeting_on()" />
|
||||
enable targeting (runs on a specific subreddit)
|
||||
</label>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var c = $(".campaign input[name=targeting]:checked");
|
||||
if (c.val() == 'one') {
|
||||
targeting_on(c);
|
||||
r.sponsored.targeting_on();
|
||||
} else {
|
||||
targeting_off(c);
|
||||
r.sponsored.targeting_off();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -349,18 +379,20 @@ ${self.javascript_setup()}
|
||||
init_enddate();
|
||||
$("#campaign").find("button[name=create]").show().end()
|
||||
.find("button[name=save]").hide().end();
|
||||
update_bid("*[name=bid]");
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="targeting" style="display:none">
|
||||
${error_field("OVERSOLD", "sr", "div")}
|
||||
${thing.subreddit_selector}
|
||||
</div>
|
||||
|
||||
<div class="notes">
|
||||
<ul>
|
||||
<li>You will only be charged for the portion of your budget that is actually spent. Any unspent portion will be refunded.</li>
|
||||
<li>By targeting, your ad will only appear in front of users who subscribe to the subreddit that you specify.</li>
|
||||
<li>Your ad will also appear at the top of the hot listing for that subreddit</li>
|
||||
<li>You can only target one subreddit per campaign. If you would like to submit to more than one subreddit, add a new campaign (its easy, you just fill this form out again).</li>
|
||||
</ul>
|
||||
${error_field("OVERSOLD", "sr", "div")}
|
||||
${SubredditSelector()}
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
@@ -403,13 +435,13 @@ ${self.javascript_setup()}
|
||||
<th title="${start_title}">start</th>
|
||||
<th title="${end_title}">end</th>
|
||||
<th>duration</th>
|
||||
<th>bid</th>
|
||||
<th>total budget</th>
|
||||
<th title="${targeting_title}">targeting</th>
|
||||
<th style="align:right">
|
||||
<button class="new-campaign fancybutton"
|
||||
${'disabled="disabled"' if len(thing.campaigns) >= g.MAX_CAMPAIGNS_PER_LINK else ''}
|
||||
title="${newcamp_title}"
|
||||
onclick="return create_campaign(this)">+ add new</button>
|
||||
onclick="return create_campaign()">+ add new</button>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -419,7 +451,7 @@ ${self.javascript_setup()}
|
||||
%for rc in sorted(thing.campaigns, key=lambda rc: rc.start_date):
|
||||
$.new_campaign(${unsafe(','.join(simplejson.dumps(attr) for attr in
|
||||
[rc.campaign_id36, rc.start_date, rc.end_date, rc.duration,
|
||||
rc.bid, rc.sr, rc.status]))});
|
||||
rc.bid, rc.cpm, rc.sr, rc.status]))});
|
||||
%endfor
|
||||
$.set_up_campaigns();
|
||||
});
|
||||
@@ -446,7 +478,8 @@ ${self.javascript_setup()}
|
||||
<th>transaction id</th>
|
||||
<th>campaign id</th>
|
||||
<th>pay id</th>
|
||||
<th>amount</th>
|
||||
<th>bid</th>
|
||||
<th>charge</th>
|
||||
<th>status</th>
|
||||
</tr>
|
||||
%for bid in thing.bids:
|
||||
@@ -457,6 +490,7 @@ ${self.javascript_setup()}
|
||||
<td>${bid.campaign}</td>
|
||||
<td>${bid.pay_id}</td>
|
||||
<td>${bid.amount_str}</td>
|
||||
<td>${bid.charge_str}</td>
|
||||
<td class="bid-status">${bid.status}</td>
|
||||
</tr>
|
||||
%endfor
|
||||
|
||||
Reference in New Issue
Block a user