From ff1eb45b6ae4fc5abf19de355a156773ad1b87ca Mon Sep 17 00:00:00 2001 From: Brian Simpson Date: Fri, 11 Oct 2013 08:42:05 -0400 Subject: [PATCH] Add priorities to PromoCampaign. --- r2/r2/controllers/promotecontroller.py | 84 ++++++++---- r2/r2/lib/pages/pages.py | 11 +- r2/r2/lib/promote.py | 73 ++++++---- r2/r2/lib/validator/validator.py | 9 ++ r2/r2/models/promo.py | 79 ++++++++++- r2/r2/public/static/css/reddit.less | 1 + r2/r2/public/static/js/sponsored.js | 179 ++++++++++++++++++------- r2/r2/templates/promotelinkform.html | 29 +++- 8 files changed, 356 insertions(+), 109 deletions(-) diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py index f1dcab0ea..7007c8b56 100644 --- a/r2/r2/controllers/promotecontroller.py +++ b/r2/r2/controllers/promotecontroller.py @@ -78,6 +78,7 @@ from r2.lib.validator import ( VLink, VModhash, VOneOf, + VPriority, VPromoCampaign, VRatelimit, VSelfText, @@ -129,6 +130,9 @@ def _check_dates(dates): def campaign_has_oversold_error(form, campaign): + if campaign.priority.inventory_override: + return + target = Subreddit._by_name(campaign.sr_name) if campaign.sr_name else None return has_oversold_error(form, campaign._id, campaign.start_date, campaign.end_date, campaign.bid, campaign.cpm, @@ -184,6 +188,17 @@ class PromoteController(ListingController): sr_names = sorted([sr.name for sr in srs], key=lambda s: s.lower()) return sr_names + @classmethod + @memoize('house_campaigns', time=60) + def get_house_campaigns(cls): + now = promote.promo_datetime_now() + pws = PromotionWeights.get_campaigns(now) + campaign_ids = {pw.promo_idx for pw in pws} + campaigns = PromoCampaign._byID(campaign_ids, data=True, + return_dict=False) + campaigns = [camp for camp in campaigns if not camp.priority.cpm] + return campaigns + @property def menus(self): filters = [ @@ -240,6 +255,10 @@ class PromoteController(ListingController): return [Link._fullname_from_id36(to36(id)) for id in link_ids] elif self.sort == 'reported': return queries.get_reported_links(get_promote_srid()) + elif self.sort == 'house': + campaigns = self.get_house_campaigns() + link_ids = {camp.link_id for camp in campaigns} + return [Link._fullname_from_id36(to36(id)) for id in link_ids] return queries.get_all_promoted_links() else: if self.sort == "future_promos": @@ -558,9 +577,10 @@ class PromoteController(ListingController): coerce=False, error=errors.BAD_BID), sr=VSubmitSR('sr', promotion=True), campaign_id36=nop("campaign_id36"), - targeting=VLength("targeting", 10)) + targeting=VLength("targeting", 10), + priority=VPriority("priority")) def POST_edit_campaign(self, form, jquery, link, campaign_id36, - dates, bid, sr, targeting): + dates, bid, sr, targeting, priority): if not link: return @@ -601,29 +621,34 @@ class PromoteController(ListingController): form.has_errors('title', errors.TOO_MANY_CAMPAIGNS) return - if form.has_errors('bid', errors.BAD_BID): - return - + campaign = None if campaign_id36: - # you cannot edit the bid of a live ad unless it's a freebie try: campaign = PromoCampaign._byID36(campaign_id36) - if (bid != campaign.bid and - campaign.start_date < datetime.now(g.tz) and - not campaign.is_freebie()): - c.errors.add(errors.BID_LIVE, field='bid') - form.has_errors('bid', errors.BID_LIVE) - return except NotFound: pass - 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_bid, - 'max': g.max_promote_bid}) - form.has_errors('bid', errors.BAD_BID) - return + if priority.cpm: + if form.has_errors('bid', errors.BAD_BID): + return + + # you cannot edit the bid of a live ad unless it's a freebie + if (campaign and bid != campaign.bid and + campaign.start_date < datetime.now(g.tz) and + not campaign.is_freebie()): + c.errors.add(errors.BID_LIVE, field='bid') + form.has_errors('bid', errors.BID_LIVE) + return + + 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_bid, + 'max': g.max_promote_bid}) + form.has_errors('bid', errors.BAD_BID) + return + else: + bid = 0. # Set bid to 0 as dummy value if targeting == 'one': if form.has_errors('sr', errors.SUBREDDIT_NOEXIST, @@ -646,23 +671,24 @@ class PromoteController(ListingController): sr = None # Check inventory - campaign_id = campaign._id if campaign_id36 else None - if has_oversold_error(form, campaign_id, start, end, bid, cpm, sr): + campaign_id = campaign._id if campaign else None + if (not priority.inventory_override and + has_oversold_error(form, campaign_id, start, end, bid, cpm, sr)): return - if campaign_id36 is not None: - campaign = PromoCampaign._byID36(campaign_id36) - promote.edit_campaign(link, campaign, dates, bid, cpm, sr) + if campaign: + promote.edit_campaign(link, campaign, dates, bid, cpm, sr, priority) r = promote.get_renderable_campaigns(link, campaign) jquery.update_campaign(r.campaign_id36, r.start_date, r.end_date, - r.duration, r.bid, r.spent, r.cpm, - r.sr, r.status) + r.duration, r.bid, r.spent, r.cpm, r.sr, + r.priority_name, r.inventory_override, + r.status) else: - campaign = promote.new_campaign(link, dates, bid, cpm, sr) + campaign = promote.new_campaign(link, dates, bid, cpm, sr, priority) r = promote.get_renderable_campaigns(link, campaign) jquery.new_campaign(r.campaign_id36, r.start_date, r.end_date, - r.duration, r.bid, r.spent, r.cpm, - r.sr, r.status) + r.duration, r.bid, r.spent, r.cpm, r.sr, + r.priority_name, r.inventory_override, r.status) @validatedForm(VSponsor('link_id'), VModhash(), diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index be048ff8d..eca4ba414 100644 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -37,7 +37,12 @@ from r2.models.gold import ( days_to_pennies, gold_revenue_on, ) -from r2.models.promo import NO_TRANSACTION, PromotionLog, PromotedLinkRoadblock +from r2.models.promo import ( + NO_TRANSACTION, + PROMOTE_PRIORITIES, + PromotedLinkRoadblock, + PromotionLog, +) from r2.models.token import OAuth2Client, OAuth2AccessToken from r2.models import traffic from r2.models import ModAction @@ -3425,6 +3430,7 @@ class PromotePage(Reddit): dest='/admin/graph')) buttons.append(NavButton('report', 'report')) buttons.append(NavButton('underdelivered', 'underdelivered')) + buttons.append(NavButton('house ads', 'house')) buttons.append(NavButton('reported links', 'reported')) menu = NavMenu(buttons, base_path = '/promoted', @@ -3510,6 +3516,9 @@ class PromoteLinkForm(Templated): self.min_bid = 0 if c.user_is_sponsor else g.min_promote_bid + 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)] + # preload some inventory srnames = set() for title, names in self.subreddit_selector.subreddit_names: diff --git a/r2/r2/lib/promote.py b/r2/r2/lib/promote.py index 689fcb9cc..5200fc45a 100644 --- a/r2/r2/lib/promote.py +++ b/r2/r2/lib/promote.py @@ -186,15 +186,23 @@ def campaign_is_live(link, campaign_index): class RenderableCampaign(): def __init__(self, campaign_id36, start_date, end_date, duration, bid, - spent, cpm, sr, status): + spent, cpm, sr, priority, status): self.campaign_id36 = campaign_id36 self.start_date = start_date self.end_date = end_date self.duration = duration - self.bid = "%.2f" % bid - self.spent = "%.2f" % spent + + if priority.cpm: + self.bid = "%.2f" % bid + self.spent = "%.2f" % spent + else: + self.bid = "N/A" + self.spent = "N/A" + self.cpm = cpm self.sr = sr + self.priority_name = priority.name + self.inventory_override = priority.inventory_override self.status = status @classmethod @@ -228,7 +236,8 @@ class RenderableCampaign(): 'pay_url': pay_url(link, camp), 'view_live_url': view_live_url(link, sr), 'sponsor': user_is_sponsor, - 'live': live} + 'live': live, + 'non_cpm': not camp.priority.cpm} if transaction and transaction.is_void(): status['paid'] = False @@ -240,7 +249,7 @@ class RenderableCampaign(): status['refund_url'] = refund_url(link, camp) rc = cls(campaign_id36, start_date, end_date, duration, bid, spent, - cpm, sr, status) + cpm, sr, camp.priority, status) r.append(rc) return r @@ -353,22 +362,28 @@ 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): +def new_campaign(link, dates, bid, cpm, sr, priority): # 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]) + campaign = PromoCampaign._new(link, sr_name, bid, cpm, dates[0], dates[1], + priority) 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, data=True) - if getattr(author, "complimentary_promos", False): - free_campaign(link, campaign, c.user) + + if campaign.priority.cpm: + author = Account._byID(link.author_id, data=True) + if getattr(author, "complimentary_promos", False): + free_campaign(link, campaign, c.user) + else: + # non-cpm campaigns are never charged, so we need to fire the hook now + hooks.get_hook('promote.new_charge').call(link=link, campaign=campaign) return campaign def free_campaign(link, campaign, user): auth_campaign(link, campaign, user, -1) -def edit_campaign(link, campaign, dates, bid, cpm, sr): +def edit_campaign(link, campaign, dates, bid, cpm, sr, priority): sr_name = sr.name if sr else '' # empty string means target to all try: # if the bid amount changed, cancel any pending transactions @@ -381,16 +396,17 @@ def edit_campaign(link, campaign, dates, bid, cpm, sr): # update values in the db campaign.update(dates[0], dates[1], bid, cpm, sr_name, - campaign.trans_id, commit=True) + campaign.trans_id, priority, commit=True) - # record the transaction - text = 'updated campaign %s. (bid: %0.2f)' % (campaign._id, bid) - PromotionLog.add(link, text) + if campaign.priority.cpm: + # record the transaction + text = 'updated campaign %s. (bid: %0.2f)' % (campaign._id, bid) + PromotionLog.add(link, text) - # make it a freebie, if applicable - author = Account._byID(link.author_id, True) - if getattr(author, "complimentary_promos", False): - free_campaign(link, campaign, c.user) + # make it a freebie, if applicable + author = Account._byID(link.author_id, True) + if getattr(author, "complimentary_promos", False): + free_campaign(link, campaign, c.user) hooks.get_hook('campaign.edit').call(link=link, campaign=campaign) @@ -572,7 +588,7 @@ def accepted_campaigns(offset=0): campaigns = dict((camp._id, camp) for camp in campaign_query) for pw in promo_weights: campaign = campaigns.get(pw.promo_idx) - if not campaign or not campaign.trans_id: + if not campaign or (not campaign.trans_id and campaign.priority.cpm): continue link = accepted_links.get(campaign.link_id) if not link: @@ -580,6 +596,14 @@ def accepted_campaigns(offset=0): yield (link, campaign, pw.weight) + +def charged_or_not_needed(campaign): + # True if a campaign has a charged transaction or doesn't need one + charged = authorize.is_charged_transaction(campaign.trans_id, campaign._id) + needs_charge = campaign.priority.cpm + return charged or not needs_charge + + def get_scheduled(offset=0): """ Arguments: @@ -596,7 +620,7 @@ def get_scheduled(offset=0): error_campaigns = [] for l, campaign, weight in accepted_campaigns(offset=offset): try: - if authorize.is_charged_transaction(campaign.trans_id, campaign._id): + if charged_or_not_needed(campaign): adweight = AdWeight(l._fullname, weight, campaign._fullname) adweights.append(adweight) except Exception, e: # could happen if campaign things have corrupt data @@ -614,8 +638,7 @@ def charge_pending(offset=1): for l, camp, weight in accepted_campaigns(offset=offset): user = Account._byID(l.author_id) try: - if authorize.is_charged_transaction(camp.trans_id, camp._id): - # already charged + if charged_or_not_needed(camp): continue charge_succeeded = authorize.charge_transaction(user, camp.trans_id, @@ -641,7 +664,7 @@ def charge_pending(offset=1): def scheduled_campaigns_by_link(l, date=None): # A promotion/campaign is scheduled/live if it's in # PromotionWeights.get_campaigns(now) and - # authorize.is_charged_transaction() + # charged_or_not_needed date = date or promo_datetime_now() @@ -656,7 +679,7 @@ def scheduled_campaigns_by_link(l, date=None): for campaign_id in campaigns: try: campaign = PromoCampaign._byID(campaign_id, data=True) - if authorize.is_charged_transaction(campaign.trans_id, campaign_id): + if charged_or_not_needed(campaign): accepted.append(campaign_id) except NotFound: g.log.error("PromoCampaign %d scheduled to run on %s not found." % diff --git a/r2/r2/lib/validator/validator.py b/r2/r2/lib/validator/validator.py index de1ae7870..892b80a5a 100644 --- a/r2/r2/lib/validator/validator.py +++ b/r2/r2/lib/validator/validator.py @@ -1683,6 +1683,15 @@ class VOneOf(Validator): for s in self.options), } + +class VPriority(Validator): + def run(self, val): + if c.user_is_sponsor: + return PROMOTE_PRIORITIES.get(val, PROMOTE_DEFAULT_PRIORITY) + else: + return PROMOTE_DEFAULT_PRIORITY + + class VImageType(Validator): def run(self, img_type): if not img_type in ('png', 'jpg'): diff --git a/r2/r2/models/promo.py b/r2/r2/models/promo.py index 3ecb469c3..c75cb7e18 100644 --- a/r2/r2/models/promo.py +++ b/r2/r2/models/promo.py @@ -27,6 +27,7 @@ import json from pycassa.types import CompositeType from pylons import g, c +from pylons.i18n import _, N_ from r2.lib import filters from r2.lib.cache import sgm @@ -40,6 +41,60 @@ from r2.models.subreddit import Subreddit PROMOTE_STATUS = Enum("unpaid", "unseen", "accepted", "rejected", "pending", "promoted", "finished") +class PriorityLevel(object): + name = '' + _text = N_('') + _description = N_('') + value = 1 # Values are from 1 (highest) to 100 (lowest) + default = False + inventory_override = False + cpm = True # Non-cpm is percentage, will fill unsold impressions + + def __repr__(self): + return "" % (self.name, self.value) + + @property + def text(self): + return _(self._text) if self._text else '' + + @property + def description(self): + return _(self._description) if self._description else '' + + +class HighPriority(PriorityLevel): + name = 'high' + _text = N_('highest') + value = 5 + + +class MediumPriority(PriorityLevel): + name = 'standard' + _text = N_('standard') + value = 10 + default = True + + +class RemnantPriority(PriorityLevel): + name = 'remnant' + _text = N_('remnant') + _description = N_('lower priority, impressions are not guaranteed') + value = 20 + inventory_override = True + + +class HousePriority(PriorityLevel): + name = 'house' + _text = N_('house') + _description = N_('non-CPM, displays in all unsold impressions') + value = 30 + inventory_override = True + cpm = False + + +HIGH, MEDIUM, REMNANT, HOUSE = HighPriority(), MediumPriority(), RemnantPriority(), HousePriority() +PROMOTE_PRIORITIES = {p.name: p for p in (HIGH, MEDIUM, REMNANT, HOUSE)} +PROMOTE_DEFAULT_PRIORITY = MEDIUM @memoize("get_promote_srid") def get_promote_srid(name = 'promos'): @@ -64,6 +119,10 @@ def calc_impressions(bid, cpm_pennies): NO_TRANSACTION = 0 class PromoCampaign(Thing): + _defaults = dict( + priority_name=PROMOTE_DEFAULT_PRIORITY.name, + ) + def __getattr__(self, attr): val = Thing.__getattr__(self, attr) if attr in ('start_date', 'end_date'): @@ -72,8 +131,14 @@ class PromoCampaign(Thing): val = val.replace(tzinfo=g.tz) return val + @classmethod + def get_priority_name(cls, priority): + if not priority in PROMOTE_PRIORITIES.values(): + raise ValueError("%s is not a valid priority" % val) + return priority.name + @classmethod - def _new(cls, link, sr_name, bid, cpm, start_date, end_date): + def _new(cls, link, sr_name, bid, cpm, start_date, end_date, priority): pc = PromoCampaign(link_id=link._id, sr_name=sr_name, bid=bid, @@ -81,7 +146,8 @@ class PromoCampaign(Thing): start_date=start_date, end_date=end_date, trans_id=NO_TRANSACTION, - owner_id=link.author_id) + owner_id=link.author_id, + priority_name=cls.get_priority_name(priority)) pc._commit() return pc @@ -111,8 +177,14 @@ class PromoCampaign(Thing): # deal with pre-CPM PromoCampaigns if not hasattr(self, 'cpm'): return -1 + elif not self.priority.cpm: + return -1 return calc_impressions(self.bid, self.cpm) + @property + def priority(self): + return PROMOTE_PRIORITIES[self.priority_name] + def is_freebie(self): return self.trans_id < 0 @@ -121,13 +193,14 @@ class PromoCampaign(Thing): return self.start_date < now and self.end_date > now def update(self, start_date, end_date, bid, cpm, sr_name, trans_id, - commit=True): + priority, 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 + self.priority_name = self.get_priority_name(priority) if commit: self._commit() diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less index 56a30bbec..30a4b5ec0 100755 --- a/r2/r2/public/static/css/reddit.less +++ b/r2/r2/public/static/css/reddit.less @@ -4521,6 +4521,7 @@ ul.tabmenu.formtab { font-size: small; padding: 4px; padding-top: 8px; + width: 90px; } .linefield .campaign input[type=text] { font-size: x-small; diff --git a/r2/r2/public/static/js/sponsored.js b/r2/r2/public/static/js/sponsored.js index c7ad76d6a..9420b6311 100644 --- a/r2/r2/public/static/js/sponsored.js +++ b/r2/r2/public/static/js/sponsored.js @@ -1,9 +1,9 @@ r.sponsored = { - init: function() { + init: function(isSponsor) { $("#sr-autocomplete").on("sr-changed blur", function() { r.sponsored.fill_campaign_editor() }) - + this.isSponsor = isSponsor this.inventory = {} }, @@ -64,7 +64,7 @@ r.sponsored = { } }, - get_booked_inventory: function($form, srname) { + get_booked_inventory: function($form, srname, isOverride) { var campaign_id36 = $form.find('input[name="campaign_id36"]').val(), campaign_row = $('.existing-campaigns .campaign-row input[name="campaign_id36"]') .filter('*[value="' + campaign_id36 + '"]') @@ -83,6 +83,11 @@ r.sponsored = { return {} } + var existingOverride = campaign_row.find('*[name="override"]').val() + if (isOverride != existingOverride) { + return {} + } + var startdate = campaign_row.find('*[name="startdate"]').val(), enddate = campaign_row.find('*[name="enddate"]').val(), dates = this.get_dates(startdate, enddate), @@ -101,7 +106,7 @@ r.sponsored = { }, - check_inventory: function($form) { + check_inventory: function($form, isOverride) { var bid = this.get_bid($form), cpm = this.get_cpm($form), requested = this.calc_impressions(bid, cpm), @@ -113,7 +118,7 @@ r.sponsored = { target = $form.find('*[name="sr"]').val(), srname = targeted ? target : '', dates = r.sponsored.get_dates(startdate, enddate), - booked = this.get_booked_inventory($form, srname) + booked = this.get_booked_inventory($form, srname, isOverride) // bail out in state where targeting is selected but srname // has not been entered yet @@ -124,32 +129,56 @@ r.sponsored = { $.when(r.sponsored.get_check_inventory(srname, dates)).done( function() { - 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 - })) + 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 + }), 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 + })) + var available = minDaily * ndays + } - var available = minDaily * ndays, - maxbid = r.sponsored.calc_bid(available, cpm) + var maxbid = r.sponsored.calc_bid(available, cpm) if (available < requested) { - var message = r._("We have insufficient inventory to fulfill" + - " your requested budget, target, and dates." + - " Only %(available)s impressions available" + - " on %(target)s from %(start)s to %(end)s. " + - "Maximum budget is $%(max)s." - ).format({ - available: r.utils.prettyNumber(available), - target: targeted ? srname : 'the frontpage', - start: startdate, - end: enddate, - max: maxbid - }) + if (isOverride) { + var message = r._("We expect to only have %(available)s " + + "impressions on %(target)s from %(start)s " + + "to %(end)s. We may not fully deliver." + ).format({ + available: r.utils.prettyNumber(available), + target: targeted ? srname : 'the frontpage', + start: startdate, + end: enddate + }) + $(".available-info").text('') + $(".OVERSOLD_DETAIL").text(message).show() + } else { + var message = r._("We have insufficient inventory to fulfill" + + " your requested budget, target, and dates." + + " Only %(available)s impressions available" + + " on %(target)s from %(start)s to %(end)s. " + + "Maximum budget is $%(max)s." + ).format({ + available: r.utils.prettyNumber(available), + target: targeted ? srname : 'the frontpage', + start: startdate, + end: enddate, + max: maxbid + }) - $(".available-info").text('') - $(".OVERSOLD_DETAIL").text(message).show() - r.sponsored.disable_form($form) + $(".available-info").text('') + $(".OVERSOLD_DETAIL").text(message).show() + r.sponsored.disable_form($form) + } } else { $(".available-info").text(r._("%(num)s available (maximum budget is $%(max)s)").format({num: r.utils.prettyNumber(available), max: maxbid})) $(".OVERSOLD_DETAIL").hide() @@ -200,7 +229,10 @@ r.sponsored = { bid = this.get_bid($form), cpm = this.get_cpm($form), ndays = this.get_duration($form), - impressions = this.calc_impressions(bid, cpm); + impressions = this.calc_impressions(bid, cpm), + selected = $form.find('*[name="priority"]:checked'), + isOverride = selected.attr("override") == "override", + isCpm = selected.attr("cpm") == "cpm" $(".duration").text(ndays + " " + ((ndays > 1) ? r._("days") : r._("day"))) $(".price-info").text(r._("$%(cpm)s per 1,000 impressions").format({cpm: (cpm/100).toFixed(2)})) @@ -208,8 +240,14 @@ r.sponsored = { $(".OVERSOLD").hide() this.enable_form($form) - this.check_bid($form) - this.check_inventory($form) + + if (isCpm) { + this.show_cpm() + this.check_bid($form) + this.check_inventory($form, isOverride) + } else { + this.hide_cpm() + } }, disable_form: function($form) { @@ -224,6 +262,24 @@ r.sponsored = { .removeClass("disabled"); }, + hide_cpm: function() { + var priceRow = $('#cpm').parent('td').parent('tr'), + budgetRow = $('#bid').parent('td').parent('tr'), + impressionsRow = $('#impressions').parent('td').parent('tr') + priceRow.hide("slow") + budgetRow.hide("slow") + impressionsRow.hide("slow") + }, + + show_cpm: function() { + var priceRow = $('#cpm').parent('td').parent('tr'), + budgetRow = $('#bid').parent('td').parent('tr'), + impressionsRow = $('#impressions').parent('td').parent('tr') + priceRow.show("slow") + budgetRow.show("slow") + impressionsRow.show("slow") + }, + targeting_on: function() { $('.targeting').find('*[name="sr"]').prop("disabled", "").end().slideDown(); this.fill_campaign_editor() @@ -234,6 +290,10 @@ r.sponsored = { this.fill_campaign_editor() }, + priority_changed: function() { + this.fill_campaign_editor() + }, + check_bid: function($form) { var bid = this.get_bid($form), minimum_bid = $("#bid").data("min_bid"); @@ -338,11 +398,14 @@ function get_flag_class(flags) { if (flags.refund) { css_class += " refund"; } + if (flags.non_cpm) { + css_class += " non_cpm"; + } return css_class } $.new_campaign = function(campaign_id36, start_date, end_date, duration, - bid, spent, cpm, targeting, flags) { + bid, spent, cpm, targeting, priority, override, flags) { cancel_edit(function() { var data =('' + @@ -352,6 +415,8 @@ $.new_campaign = function(campaign_id36, start_date, end_date, duration, '' + '' + + '' + + '' + ''); if (flags && flags.pay_url) { data += (""); } - var row = [start_date, end_date, duration, "$" + bid, "$" + spent, targeting, data]; + var row = [start_date, end_date, duration, priority, "$" + bid, "$" + spent, targeting, data]; $(".existing-campaigns .error").hide(); var css_class = get_flag_class(flags); $(".existing-campaigns table").show() @@ -378,7 +443,8 @@ $.new_campaign = function(campaign_id36, start_date, end_date, duration, }; $.update_campaign = function(campaign_id36, start_date, end_date, - duration, bid, spent, cpm, targeting, flags) { + duration, bid, spent, cpm, targeting, priority, + override, flags) { cancel_edit(function() { $('.existing-campaigns input[name="campaign_id36"]') .filter('*[value="' + (campaign_id36 || '0') + '"]') @@ -387,6 +453,7 @@ $.update_campaign = function(campaign_id36, start_date, end_date, .children(":first").html(start_date) .next().html(end_date) .next().html(duration) + .next().html(priority) .next().html("$" + bid).removeClass() .next().html("$" + spent) .next().html(targeting) @@ -394,6 +461,8 @@ $.update_campaign = function(campaign_id36, start_date, end_date, .find('*[name="startdate"]').val(start_date).end() .find('*[name="enddate"]').val(end_date).end() .find('*[name="targeting"]').val(targeting).end() + .find('*[name="priority"]').val(priority).end() + .find('*[name="override"]').val(override).end() .find('*[name="bid"]').val(bid).end() .find('*[name="cpm"]').val(cpm).end() .find("button, span").remove(); @@ -412,7 +481,7 @@ $.set_up_campaigns = function() { $(".existing-campaigns tr").each(function() { var tr = $(this); var td = $(this).find("td:last"); - var bid_td = $(this).find("td:first").next().next().next() + var bid_td = $(this).find("td:first").next().next().next().next() .addClass("bid"); if(td.length && ! td.children("button, span").length ) { if(tr.hasClass("live")) { @@ -427,20 +496,22 @@ $.set_up_campaigns = function() { /* once paid, we shouldn't muck around with the campaign */ if(!tr.hasClass("complete") && !tr.hasClass("live")) { - if (tr.hasClass("sponsor") && !tr.hasClass("free")) { - $(bid_td).append($(free).addClass("free") - .click(function() { free_campaign(tr) })) - } - else if (!tr.hasClass("paid")) { - $(bid_td).prepend($(pay).addClass("pay fancybutton") - .click(function() { pay_campaign(tr) })); - } else if (tr.hasClass("free")) { - $(bid_td).addClass("free paid") - .prepend("freebie"); - } else { - (bid_td).addClass("paid") - .prepend($(repay).addClass("pay fancybutton") - .click(function() { pay_campaign(tr) })); + if (!tr.hasClass("non_cpm")) { + if (tr.hasClass("sponsor") && !tr.hasClass("free")) { + $(bid_td).append($(free).addClass("free") + .click(function() { free_campaign(tr) })) + } + else if (!tr.hasClass("paid")) { + $(bid_td).prepend($(pay).addClass("pay fancybutton") + .click(function() { pay_campaign(tr) })); + } else if (tr.hasClass("free")) { + $(bid_td).addClass("free paid") + .prepend("freebie"); + } else { + (bid_td).addClass("paid") + .prepend($(repay).addClass("pay fancybutton") + .click(function() { pay_campaign(tr) })); + } } var e = $(edit).addClass("edit fancybutton") .click(function() { edit_campaign(tr); }); @@ -451,7 +522,9 @@ $.set_up_campaigns = function() { if (tr.hasClass("complete")) { $(td).append("complete"); } - $(bid_td).addClass("paid") + if (!tr.hasClass("non_cpm")) { + $(bid_td).addClass("paid") + } /* sponsors can always edit */ if (tr.hasClass("sponsor")) { var e = $(edit).addClass("edit fancybutton") @@ -461,6 +534,9 @@ $.set_up_campaigns = function() { } } }); + if (!r.sponsored.isSponsor) { + $('.existing-campaigns tr td:nth-child(4), .existing-campaigns tr th:nth-child(4)').hide() + } return $; } @@ -543,6 +619,10 @@ function edit_campaign(elem) { i = '*[name="' + i + '"]'; c.find(i).val(data_tr.find(i).val()); }); + var priorities = c.find('*[name="priority"]'), + campPriority = data_tr.find('*[name="priority"]').val() + priorities.filter('*[value="' + campPriority + '"]') + .prop("checked", "checked") /* check if targeting is turned on */ var targeting = data_tr .find('*[name="targeting"]').val(); @@ -598,6 +678,7 @@ function create_campaign() { .find('input[name="campaign_id36"]').val('').end() .find('input[name="sr"]').val('').prop("disabled", "disabled").end() .find('input[name="targeting"][value="none"]').prop("checked", "checked").end() + .find('input[name="priority"][default="default"]').prop("checked", "checked").end() .find(".targeting").hide().end() .find('input[name="cpm"]').val(base_cpm).end() .fadeIn(); diff --git a/r2/r2/templates/promotelinkform.html b/r2/r2/templates/promotelinkform.html index 5451e01a5..f452dab49 100644 --- a/r2/r2/templates/promotelinkform.html +++ b/r2/r2/templates/promotelinkform.html @@ -42,7 +42,7 @@ ${unsafe(js.use('sponsored'))} <%def name="javascript_setup()"> @@ -344,6 +344,29 @@ ${self.javascript_setup()} + + ${_("priority")} + + %for value, text, description, default, inventory_override, cpm in thing.priorities: + +
+ %endfor + + + ${_("budget")} @@ -469,6 +492,7 @@ ${self.javascript_setup()} ${_("start")} ${_("end")} ${_("duration")} + ${_("priority")} ${_("total budget")} ${_("spent")} ${_("targeting")} @@ -486,7 +510,8 @@ ${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.spent, rc.cpm, rc.sr, rc.status]))}); + rc.bid, rc.spent, rc.cpm, rc.sr, rc.priority_name, + rc.inventory_override, rc.status]))}); %endfor $.set_up_campaigns(); });