Redesign PromotedLinkTraffic.

This commit is contained in:
bsimpson63
2013-04-11 14:22:42 -04:00
parent 00623614c9
commit bc23801d4e
6 changed files with 320 additions and 117 deletions

View File

@@ -149,8 +149,8 @@ def make_map():
action='related', title=None)
mc('/details/:article/:title', controller='front',
action='details', title=None)
mc('/traffic/:article/:title', controller='front',
action='traffic', title=None)
mc('/traffic/:link/:campaign', controller='front', action='traffic',
campaign=None)
mc('/comments/:article/:title/:comment', controller='front',
action='comments', title=None, comment=None)
mc('/duplicates/:article/:title', controller='front',

View File

@@ -1004,21 +1004,18 @@ class FrontController(RedditController, OAuth2ResourceController):
@require_oauth2_scope("modtraffic")
@validate(VTrafficViewer('article'),
article=VLink('article'),
@validate(VTrafficViewer('link'),
link=VLink('link'),
campaign=VPromoCampaign('campaign'),
before=VDate('before', format='%Y%m%d%H'),
after=VDate('after', format='%Y%m%d%H'))
def GET_traffic(self, article, before, after):
if before:
before = before.replace(tzinfo=None)
if after:
after = after.replace(tzinfo=None)
content = trafficpages.PromotedLinkTraffic(article, before, after)
def GET_traffic(self, link, campaign, before, after):
if c.render_style == 'csv':
return content.as_csv()
return trafficpages.PromotedLinkTraffic.as_csv(campaign or link)
return LinkInfoPage(link=article,
content = trafficpages.PromotedLinkTraffic(link, campaign, before,
after)
return LinkInfoPage(link=link,
page_classes=["promoted-traffic"],
comment=None,
content=content).render()
@@ -1031,7 +1028,7 @@ class FrontController(RedditController, OAuth2ResourceController):
if c.render_style == 'csv':
return content.as_csv()
return LinkInfoPage(link=link,
page_classes=["promo-traffic"],
page_classes=["promoted-traffic"],
comment=None,
content=content).render()
else:

View File

@@ -30,13 +30,15 @@ from pylons.i18n import _
from pylons import g, c, request
import babel.core
from babel.dates import format_datetime
from babel.numbers import format_currency
from r2.lib import promote
from r2.lib.menus import menu
from r2.lib.menus import NavButton, NamedButton, PageNameNav, NavMenu
from r2.lib.pages.pages import Reddit, TimeSeriesChart, UserList, TabbedPane
from r2.lib.promote import cost_per_mille, cost_per_click
from r2.lib.utils import Storage
from r2.lib.template_helpers import format_number
from r2.lib.utils import Storage, to_date
from r2.lib.wrapped import Templated
from r2.models import Thing, Link, PromoCampaign, traffic
from r2.models.subreddit import Subreddit, _DefaultSR
@@ -452,83 +454,224 @@ def _is_promo_preliminary(end_date):
return end_date + datetime.timedelta(days=1) > now
class PromotedLinkTraffic(RedditTraffic):
def __init__(self, thing, before=None, after=None):
def get_traffic_dates(thing):
"""Retrieve the start and end of a Promoted Link or PromoCampaign."""
now = datetime.datetime.now(g.tz).replace(minute=0, second=0,
microsecond=0)
if isinstance(thing, Link):
start, end = promote.get_total_run(thing)
start, end = start.replace(tzinfo=g.tz), end.replace(tzinfo=g.tz)
elif isinstance(thing, PromoCampaign):
# PromoCampaigns store their dates as UTC, promote changes occur
# at 12 AM EST
promo_tz = pytz.timezone("US/Eastern")
start = (thing.start_date.replace(tzinfo=promo_tz)
.astimezone(pytz.utc))
end = (thing.end_date.replace(tzinfo=promo_tz)
.astimezone(pytz.utc))
end = min(now, end)
return start, end
def get_promo_traffic(thing, start, end):
"""Get traffic for a Promoted Link or PromoCampaign"""
if isinstance(thing, Link):
imp_fn = traffic.AdImpressionsByCodename.promotion_history
click_fn = traffic.ClickthroughsByCodename.promotion_history
elif isinstance(thing, PromoCampaign):
imp_fn = traffic.TargetedImpressionsByCodename.promotion_history
click_fn = traffic.TargetedClickthroughsByCodename.promotion_history
imps = imp_fn(thing._fullname, start.replace(tzinfo=None),
end.replace(tzinfo=None))
clicks = click_fn(thing._fullname, start.replace(tzinfo=None),
end.replace(tzinfo=None))
if imps and not clicks:
clicks = [(imps[0][0], (0,))]
history = traffic.zip_timeseries(imps, clicks, order="ascending")
return history
def get_billable_traffic(campaign):
"""Get traffic for dates when PromoCampaign is active."""
start, end = get_traffic_dates(campaign)
return get_promo_traffic(campaign, start, end)
def is_early_campaign(campaign):
# traffic by campaign was only recorded starting 2012/9/12
return campaign.end_date < datetime.datetime(2012, 9, 12, 0, 0, tzinfo=g.tz)
def is_launched_campaign(campaign):
now = datetime.datetime.now(g.tz).date()
return bool(campaign.trans_id) and campaign.start_date.date() <= now
class PromotedLinkTraffic(Templated):
def __init__(self, thing, campaign, before, after):
self.thing = thing
self.campaign = campaign
self.before = before
self.after = after
self.period = datetime.timedelta(days=31)
self.period = datetime.timedelta(days=7)
self.prev = None
self.next = None
self.has_live_campaign = False
self.has_early_campaign = False
self.detail_name = ('campaign %s' % campaign._id36 if campaign
else 'all campaigns')
editable = c.user_is_sponsor or c.user._id == thing.author_id
self.viewer_list = TrafficViewerList(thing, editable)
RedditTraffic.__init__(self, None)
self.traffic_last_modified = traffic.get_traffic_last_modified()
self.traffic_lag = (datetime.datetime.utcnow() -
self.traffic_last_modified)
self.make_hourly_table(campaign or thing)
self.make_campaign_table()
Templated.__init__(self)
def make_tables(self):
now = datetime.datetime.utcnow().replace(minute=0, second=0,
microsecond=0)
@classmethod
def make_campaign_table_row(cls, id, start, end, target, bid, impressions,
clicks, is_live, is_active, url, is_total):
if impressions:
cpm = format_currency(promote.cost_per_mille(bid, impressions),
'USD', locale=c.locale)
else:
cpm = '---'
promo_start, promo_end = promote.get_total_run(self.thing)
promo_end = min(now, promo_end)
if clicks:
cpc = format_currency(promote.cost_per_click(bid, clicks), 'USD',
locale=c.locale)
ctr = format_number(_clickthrough_rate(impressions, clicks))
else:
cpc = '---'
ctr = '---'
if not promo_start or not promo_end:
self.history = []
return
return {
'id': id,
'start': start,
'end': end,
'target': target,
'bid': format_currency(bid, 'USD', locale=c.locale),
'impressions': format_number(impressions),
'cpm': cpm,
'clicks': format_number(clicks),
'cpc': cpc,
'ctr': ctr,
'live': is_live,
'active': is_active,
'url': url,
'csv': url + '.csv',
'total': is_total,
}
def make_campaign_table(self):
campaigns = PromoCampaign._by_link(self.thing._id)
total_bid = 0
total_impressions = 0
total_clicks = 0
self.campaign_table = []
for camp in campaigns:
if not is_launched_campaign(camp):
continue
is_live = camp.is_live_now()
self.has_early_campaign |= is_early_campaign(camp)
self.has_live_campaign |= is_live
history = get_billable_traffic(camp)
impressions, clicks = 0, 0
for date, (imp, click) in history:
impressions += imp
clicks += click
start = to_date(camp.start_date).strftime('%Y-%m-%d')
end = to_date(camp.end_date).strftime('%Y-%m-%d')
target = camp.sr_name or 'frontpage'
is_active = self.campaign and self.campaign._id36 == camp._id36
url = '/traffic/%s/%s' % (self.thing._id36, camp._id36)
is_total = False
row = self.make_campaign_table_row(camp._id36, start, end, target,
camp.bid, impressions, clicks,
is_live, is_active, url,
is_total)
self.campaign_table.append(row)
total_bid += camp.bid
total_impressions += impressions
total_clicks += clicks
# total row
start = '---'
end = '---'
target = '---'
is_live = False
is_active = not self.campaign
url = '/traffic/%s' % self.thing._id36
is_total = True
row = self.make_campaign_table_row(_('total'), start, end, target,
total_bid, total_impressions,
total_clicks, is_live, is_active,
url, is_total)
self.campaign_table.append(row)
def check_dates(self, thing):
"""Shorten range for display and add next/prev buttons."""
start, end = get_traffic_dates(thing)
if self.period:
start = self.after
end = self.before
display_start = self.after
display_end = self.before
if not start and not end:
end = promo_end
start = end - self.period
if not display_start and not display_end:
display_end = end
display_start = end - self.period
elif not display_end:
display_end = display_start + self.period
elif not display_start:
display_start = display_end - self.period
elif not end:
end = start + self.period
elif not start:
start = end - self.period
if start > promo_start:
if display_start > start:
p = request.get.copy()
p.update({'after':None, 'before':start.strftime('%Y%m%d%H')})
p.update({
'after': None,
'before': display_start.strftime('%Y%m%d%H'),
})
self.prev = '%s?%s' % (request.path, urllib.urlencode(p))
else:
start = promo_start
display_start = start
if end < promo_end:
if display_end < end:
p = request.get.copy()
p.update({'after':end.strftime('%Y%m%d%H'), 'before':None})
p.update({
'after': display_end.strftime('%Y%m%d%H'),
'before': None,
})
self.next = '%s?%s' % (request.path, urllib.urlencode(p))
else:
end = promo_end
display_end = end
else:
start, end = promo_start, promo_end
display_start, display_end = start, end
fullname = self.thing._fullname
imps = traffic.AdImpressionsByCodename.promotion_history(fullname,
start, end)
clicks = traffic.ClickthroughsByCodename.promotion_history(fullname,
start, end)
return display_start, display_end
# promotion might have no clicks, zip_timeseries needs valid columns
if imps and not clicks:
clicks = [(imps[0][0], (0, 0))]
history = traffic.zip_timeseries(imps, clicks, order="ascending")
@classmethod
def get_hourly_traffic(cls, thing, start, end):
"""Retrieve hourly traffic for a Promoted Link or PromoCampaign."""
history = get_promo_traffic(thing, start, end)
computed_history = []
self.total_impressions, self.total_clicks = 0, 0
for date, data in history:
u_imps, imps, u_clicks, clicks = data
u_ctr = _clickthrough_rate(u_imps, u_clicks)
imps, clicks = data
ctr = _clickthrough_rate(imps, clicks)
self.total_impressions += imps
self.total_clicks += clicks
date = date.replace(tzinfo=pytz.utc)
date = date.astimezone(pytz.timezone("US/Eastern"))
datestr = format_datetime(
@@ -536,14 +679,21 @@ class PromotedLinkTraffic(RedditTraffic):
locale=c.locale,
format="yyyy-MM-dd HH:mm zzz",
)
computed_history.append((date, datestr, data + (u_ctr, ctr)))
computed_history.append((date, datestr, data + (ctr,)))
return computed_history
self.history = computed_history
def make_hourly_table(self, thing):
start, end = self.check_dates(thing)
self.history = self.get_hourly_traffic(thing, start, end)
self.total_impressions, self.total_clicks = 0, 0
for date, datestr, data in self.history:
imps, clicks, ctr = data
self.total_impressions += imps
self.total_clicks += clicks
if self.total_impressions > 0:
self.total_ctr = _clickthrough_rate(self.total_impressions,
self.total_clicks)
# XXX: _is_promo_preliminary correctly expects tz-aware datetimes
# because it's also used with datetimes from promo code. this hack
# relies on the fact that we're storing UTC w/o timezone info.
@@ -551,31 +701,25 @@ class PromotedLinkTraffic(RedditTraffic):
end_aware = end.replace(tzinfo=g.tz)
self.is_preliminary = _is_promo_preliminary(end_aware)
# we should only graph a sane number of data points (not everything)
self.max_points = traffic.points_for_interval("hour")
return computed_history
def as_csv(self):
@classmethod
def as_csv(cls, thing):
"""Return the traffic data in CSV format for reports."""
import csv
import cStringIO
start, end = get_traffic_dates(thing)
history = cls.get_hourly_traffic(thing, start, end)
out = cStringIO.StringIO()
writer = csv.writer(out)
self.period = None
history = self.make_tables()
writer.writerow((_("date and time (UTC)"),
_("unique impressions"),
_("total impressions"),
_("unique clicks"),
_("total clicks"),
_("unique click-through rate (%)"),
_("total click-through rate (%)")))
_("impressions"),
_("clicks"),
_("click-through rate (%)")))
for date, datestr, values in history:
# flatten (date, value-tuple) to (date, value1, value2...)
# flatten (date, datestr, value-tuple) to (date, value1, value2...)
writer.writerow((date,) + values)
return out.getvalue()

View File

@@ -165,6 +165,7 @@ apps below.
traffic_promoted_link_explanation = _("Below you will see your promotion's impression and click traffic per hour of promotion. Please note that these traffic totals will lag behind by two to three hours, and that daily totals will be preliminary until 24 hours after the link has finished its run."),
traffic_processing_slow = _("Traffic processing is currently running slow. The latest data available is from %(date)s. This page will be updated as new data becomes available."),
traffic_processing_normal = _("Traffic processing occurs on an hourly basis. The latest data available is from %(date)s. This page will be updated as new data becomes available."),
traffic_help_email = _("Questions? Email self serve support: %(email)s"),
traffic_subreddit_explanation = _("""
Below are the traffic statistics for your subreddit. Each graph represents one of the following over the interval specified.

View File

@@ -4214,6 +4214,28 @@ div.timeseries span.title {
white-space: nowrap;
}
.traffic-table.promocampaign-table {
thead th {
text-align: right;
padding: 0 5px;
}
tr.total {
border-top: 1px solid black;
}
tr.active {
background-color: pink;
font-weight: bold;
border: 2px dotted red;
}
}
.traffic_viewer-table {
margin-bottom: 2em;
margin-left: 1em;
}
.promo-traffic .content .tabmenu li {
font-size: 1.3em;
}

View File

@@ -20,8 +20,7 @@
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="reddittraffic.html"/>
<%namespace file="reddittraffic.html" import="load_timeseries_js"/>
<%namespace file="reddittraffic.html" import="load_timeseries_js, last_modified_message"/>
<%namespace file="utils.html" import="plain_link" />
<%!
@@ -30,40 +29,17 @@
from r2.lib.template_helpers import format_number, js_timestamp
%>
<%def name="preamble()">
${unsafe(safemarkdown(strings.traffic_promoted_link_explanation))}
${thing.viewer_list}
<h1>
${_("promotion traffic")}
<a href="/traffic/${thing.thing._id36}.csv">
${_("(download as .csv)")}
</a>
</h1>
${load_timeseries_js()}
</%def>
<%def name="sidetables()" />
<%def name="tables()">
<table id="promotion-history" class="traffic-table timeseries" data-interval="hour" data-max-points="${thing.max_points}">
<%def name="make_traffic_table()">
<div class="traffic-tables">
<h1>detailed traffic for ${thing.detail_name}</h1>
<div id="charts"></div>
<table id="promotion-history" class="traffic-table timeseries" data-interval="hour" data-max-points="${len(thing.history)}">
<thead>
<tr>
<th></th>
<th colspan="2">${_("impressions")}</th>
<th colspan="2">${_("clicks")}</th>
<th colspan="2">${_("click-through (%)")}</th>
</tr>
<tr>
<th scope="col">${_("date")}</th>
<th scope="col" title="${_("unique impressions")}">${_("unique")}</th>
<th scope="col" title="${_("total impressions")}" data-color="#ff5700">${_("total")}</th>
<th scope="col" title="${_("unique clicks")}">${_("unique")}</th>
<th scope="col" title="${_("total clicks")}" data-color="#9494ff">${_("total")}</th>
<th scope="col">${_("unique")}</th>
<th scope="col">${_("total")}</th>
<th scope="col" title="${_("impressions")}" data-color="#ff5700">${_("impressions")}</th>
<th scope="col" title="${_("clicks")}" data-color="#9494ff">${_("clicks")}</th>
<th scope="col" title="${_("click-through (%)")}">${_("click-through (%)")}</th>
</tr>
</thead>
<tbody>
@@ -84,11 +60,8 @@
*
% endif
</th>
<td>--</td>
<td>${format_number(thing.total_impressions)}</td>
<td>--</td>
<td>${format_number(thing.total_clicks)}</td>
<td>--</td>
% if thing.total_impressions != 0:
<td>${format_number(thing.total_ctr)}%</td>
% else:
@@ -103,7 +76,7 @@
% endif
${nextprev()}
</div>
</%def>
<%def name="nextprev()">
@@ -121,3 +94,69 @@
</p>
%endif
</%def>
<%def name="make_campaign_table()">
<h1>campaigns</h1>
%if thing.has_early_campaign:
<div class="promo-traffic-help">
${_("Campaigns created before September 12, 2012 don't have traffic data")}
</div>
%endif
<table class="traffic-table promocampaign-table">
<thead>
<th>${_("id")}</th>
<th></th>
<th></th>
<th>${_("start")}</th>
<th>${_("end")}</th>
<th>${_("target")}</th>
<th>${_("bid")}</th>
<th>${_("impressions")}</th>
<th>${_("cpm")}</th>
<th>${_("clicks")}</th>
<th>${_("ctr")}</th>
<th>${_("cpc")}</th>
</thead>
%for camp in thing.campaign_table:
<tr class="${'promo-traffic-live' if camp['live'] else ''} ${'active' if camp['active'] else ''} ${'total' if camp['total'] else ''}">
<td>${camp['id']}</td>
<td>${plain_link(unsafe(_("detail")), camp['url'])}</td>
<td>${plain_link(unsafe(_("csv")), camp['csv'])}</td>
<td>${camp['start']}</td>
<td>${camp['end']}</td>
<td>${camp['target']}</td>
<td>${camp['bid']}</td>
<td>${camp['impressions']}</td>
<td>${camp['cpm']}</td>
<td>${camp['clicks']}</td>
<td>${camp['ctr']}</td>
<td>${camp['cpc']}</td>
</tr>
%endfor
</table>
</%def>
${load_timeseries_js()}
${unsafe(safemarkdown(strings.traffic_promoted_link_explanation))}
%if thing.has_live_campaign:
${last_modified_message()}
%endif
${unsafe(safemarkdown(strings.traffic_help_email % dict(email=g.selfserve_support_email)))}
${thing.viewer_list}
${make_campaign_table()}
${make_traffic_table()}
<script type="text/javascript">
r.timeseries.init()
</script>