diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py index f038636dc..671277c05 100644 --- a/r2/r2/config/routing.py +++ b/r2/r2/config/routing.py @@ -142,14 +142,18 @@ def make_map(): mc('/admin/promoted', controller='promote', action='admin') mc('/promoted/edit_promo/:link', controller='promote', action='edit_promo') + mc('/promoted/edit_promo_cpm/:link', # development only (don't link to url) + controller='promote', action='edit_promo_cpm') mc('/promoted/edit_promo/pc/:campaign', controller='promote', # admin only action='edit_promo_campaign') mc('/promoted/pay/:link/:campaign', controller='promote', action='pay') mc('/promoted/graph', controller='promote', action='graph') - - mc('/promoted/traffic/headline/:link', controller='front', action='promo_traffic') + mc('/promoted/inventory/:sr_name', + controller='promote', action='inventory') + mc('/promoted/traffic/headline/:link', + controller='front', action='promo_traffic') mc('/promoted/:action', controller='promote', requirements=dict(action="edit_promo|new_promo|roadblock")) diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py index 2608954f9..e50c466fc 100644 --- a/r2/r2/controllers/promotecontroller.py +++ b/r2/r2/controllers/promotecontroller.py @@ -20,6 +20,8 @@ # Inc. All Rights Reserved. ############################################################################### +import json + from validator import * from pylons.i18n import _ from r2.models import * @@ -112,6 +114,25 @@ class PromoteController(ListingController): return page.render() + + # For development. Should eventually replace GET_edit_promo + @validate(VSponsor('link'), + link = VLink('link')) + def GET_edit_promo_cpm(self, link): + if not link or link.promoted is None: + return self.abort404() + rendered = wrap_links(link, wrapper = promote.sponsor_wrapper, + skip = False) + + form = PromoteLinkFormCpm(link = link, + listing = rendered, + timedeltatext = "") + + page = PromotePage('new_promo', content = form) + + return page.render() + + # admin only because the route might change @validate(VSponsorAdmin('campaign'), campaign=VPromoCampaign('campaign')) @@ -129,6 +150,28 @@ class PromoteController(ListingController): return c.response return PromotePage("graph", content = content).render() + + def GET_inventory(self, sr_name): + ''' + Return available inventory data as json for use in ajax calls + ''' + inv_start_date = promote.promo_datetime_now() + inv_end_date = inv_start_date + timedelta(60) + inventory = promote.get_available_impressions(sr_name, + inv_start_date, + inv_end_date, + fuzzed=(not c.user_is_admin)) + dates = [] + impressions = [] + max_imps = 0 + for date, imps in inventory.iteritems(): + dates.append(date.strftime("%m/%d/%Y")) + impressions.append(imps) + max_imps = max(max_imps, imps) + return json.dumps({'sr':sr_name, + 'dates': dates, + 'imps':impressions, + 'max_imps':max_imps}) ### POST controllers below @validatedForm(VSponsorAdmin(), diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py index bb8feaf58..ab1b5a4b5 100755 --- a/r2/r2/lib/pages/pages.py +++ b/r2/r2/lib/pages/pages.py @@ -3164,6 +3164,12 @@ class PromotePage(Reddit): class PromoteLinkForm(Templated): def __init__(self, sr=None, link=None, listing='', timedeltatext='', *a, **kw): + self.setup(sr, link, listing, timedeltatext, *a, **kw) + Templated.__init__(self, sr=sr, datefmt = datefmt, + timedeltatext=timedeltatext, listing = listing, + bids = self.bids, *a, **kw) + + def setup(self, sr, link, listing, timedeltatext, *a, **kw): bids = [] if c.user_is_sponsor and link: self.author = Account._byID(link.author_id) @@ -3211,11 +3217,24 @@ class PromoteLinkForm(Templated): self.market, self.promo_counter = \ Promote_Graph.get_market(None, start_date, end_date) + self.bids = bids self.min_daily_bid = 0 if c.user_is_admin else g.min_promote_bid + +class PromoteLinkFormCpm(PromoteLinkForm): + def __init__(self, sr=None, link=None, listing='', + timedeltatext='', *a, **kw): + self.setup(sr, link, listing, timedeltatext, *a, **kw) + + if not c.user_is_sponsor: + self.now = promote.promo_datetime_now().date() + start_date = self.now + end_date = self.now + datetime.timedelta(60) # two months + self.inventory = promote.get_available_impressions(sr, start_date, end_date) + Templated.__init__(self, sr=sr, datefmt = datefmt, timedeltatext=timedeltatext, listing = listing, - bids = bids, *a, **kw) + bids = self.bids, *a, **kw) class PromoAdminTool(Reddit): diff --git a/r2/r2/lib/promote.py b/r2/r2/lib/promote.py index 57d61f4de..f81e44e4b 100644 --- a/r2/r2/lib/promote.py +++ b/r2/r2/lib/promote.py @@ -22,6 +22,7 @@ from __future__ import with_statement +from collections import OrderedDict import json import time @@ -31,8 +32,9 @@ from r2.models.keyvalue import NamedGlobals from r2.lib.wrapped import Wrapped from r2.lib import authorize from r2.lib import emailer +from r2.lib import inventory from r2.lib.template_helpers import get_domain -from r2.lib.utils import Enum, UniqueIterator, tup +from r2.lib.utils import Enum, UniqueIterator, tup, to_date from r2.lib.organic import keep_fresh_links from pylons import g, c from datetime import datetime, timedelta @@ -538,6 +540,37 @@ def get_scheduled(offset=0): error_campaigns.append((campaign._id, e)) return {'by_sr': by_sr, 'links': links, 'error_campaigns': error_campaigns} +def fuzz_impressions(imps): + """Return imps rounded to one significant digit.""" + if imps > 0: + ndigits = int(math.floor(math.log10(imps))) + return int(round(imps, -1*ndigits)) # note the negative + else: + return 0 + +def get_scheduled_impressions(sr_name, start_date, end_date): + # FIXME: mock function for development + start_date = to_date(start_date) + end_date = to_date(end_date) + ndays = (end_date - start_date).days + scheduled = OrderedDict() + for i in range(ndays): + date = (start_date + timedelta(i)) + scheduled[date] = random.randint(0, 100) # FIXME: fakedata + return scheduled + +def get_available_impressions(sr_name, start_date, end_date, fuzzed=False): + # FIXME: mock function for development + start_date = to_date(start_date) + end_date = to_date(end_date) + available = inventory.get_predicted_by_date(sr_name, start_date, end_date) + scheduled = get_scheduled_impressions(sr_name, start_date, end_date) + for date in scheduled: + available[date] = int(available[date] - (available[date]*scheduled[date]/100.)) # DELETEME + #available[date] = max(0, available[date] - scheduled[date]) # UNCOMMENTME + if fuzzed: + available[date] = fuzz_impressions(available[date]) + return available def charge_pending(offset=1): for l, camp, weight in accepted_campaigns(offset=offset): diff --git a/r2/r2/public/static/js/reddit.js b/r2/r2/public/static/js/reddit.js index 4d5ef4cfb..018f6df02 100644 --- a/r2/r2/public/static/js/reddit.js +++ b/r2/r2/public/static/js/reddit.js @@ -810,6 +810,7 @@ function sr_name_down(e) { return false; } else if (e.keyCode == 13) { + $("#sr-autocomplete").trigger("sr-changed"); hide_sr_name_list(); input.parents("form").submit(); return false; @@ -830,12 +831,14 @@ function sr_dropdown_mup(row) { var name = $(row).text(); $("#sr-autocomplete").val(name); $("#sr-drop-down").hide(); + $("#sr-autocomplete").trigger("sr-changed"); } } function set_sr_name(link) { var name = $(link).text(); $("#sr-autocomplete").trigger('focus').val(name); + $("#sr-autocomplete").trigger("sr-changed"); } /*** tabbed pane stuff ***/ diff --git a/r2/r2/public/static/js/sponsored.js b/r2/r2/public/static/js/sponsored.js index c3a8167e6..76b8c27d2 100644 --- a/r2/r2/public/static/js/sponsored.js +++ b/r2/r2/public/static/js/sponsored.js @@ -374,3 +374,29 @@ function pay_campaign(elem) { function view_campaign(elem) { $.redirect($(elem).find('input[name="view_live_url"]').val()); } + +// writes rows into inventory table when subreddit selector changes +function update_inventory_table() { + var sr = $('#targeting').attr('checked') ? $('#sr-autocomplete').val() : ' reddit.com'; + $.ajax({ + url: '/promoted/inventory/' + sr, + type: 'GET', + dataType: 'json', + // on success, update title to show subreddit name and fill table rows + success: function(data) { // {'sr':'funny', 'dates':[...], 'imps':[...]} + var sr_name = (data['sr'] == ' reddit.com') ? 'front page' : data['sr']; + $('#inventory-title > span').text('available ' + sr_name + ' impressions'); // FIXME: i18n + $('#inventory').empty(); + $('#inventory').append('