PromoteReport for generating traffic reports on promoted links.

This commit is contained in:
bsimpson63
2013-04-16 13:42:49 -04:00
parent 9442f16cb4
commit a2f0a9e35c
5 changed files with 376 additions and 2 deletions

View File

@@ -181,6 +181,7 @@ def make_map():
mc('/promoted/:action', controller='promote',
requirements=dict(action="edit_promo|new_promo|roadblock"))
mc('/promoted/report', controller='promote', action='report')
mc('/promoted/:sort/:sr', controller='promote', action='listing',
requirements=dict(sort='live_promos'))
mc('/promoted/:sort', controller='promote', action="listing")

View File

@@ -44,6 +44,8 @@ from r2.lib.pages import (
PromotePage,
PromoteLinkForm,
PromoteLinkFormCpm,
PromoteReport,
Reddit,
Roadblocks,
UploadedImage,
)
@@ -758,4 +760,31 @@ class PromoteController(ListingController):
start=dates[0],
end=dates[1]).render()
@validate(VSponsorAdmin(),
start=VDate('startdate'),
end=VDate('enddate'),
link_text=nop('link_text'))
def GET_report(self, start, end, link_text=None):
now = datetime.now(g.tz).replace(hour=0, minute=0, second=0,
microsecond=0)
end = end or now - timedelta(days=1)
start = start or end - timedelta(days=7)
if link_text is not None:
names = link_text.replace(',', ' ').split()
try:
links = Link._by_fullname(names, data=True)
except NotFound:
links = {}
bad_links = [name for name in names if name not in links]
links = links.values()
else:
links = []
bad_links = []
content = PromoteReport(links, link_text, bad_links, start, end)
if c.render_style == 'csv':
return content.as_csv()
else:
return PromotePage('report', content=content).render()

View File

@@ -70,10 +70,15 @@ from r2.lib.memoize import memoize
from r2.lib.utils import trunc_string as _truncate, to_date
from r2.lib.filters import safemarkdown
from babel.numbers import format_currency
from collections import defaultdict
import csv
import cStringIO
import pytz
import sys, random, datetime, calendar, simplejson, re, time
import time
from itertools import chain
from urllib import quote
from itertools import chain, product
from urllib import quote, urlencode
# the ip tracking code is currently deeply tied with spam prevention stuff
# this will be open sourced as soon as it can be decoupled
@@ -3170,6 +3175,7 @@ class PromotePage(Reddit):
if c.user_is_sponsor:
buttons.append(NamedButton('admin_graph',
dest='/admin/graph'))
buttons.append(NavButton('report', 'report'))
menu = NavMenu(buttons, base_path = '/promoted',
type='flatlist')
@@ -3735,6 +3741,151 @@ class Promote_Graph(Templated):
"$%.2f" % link.promote_bid,
_force_unicode(link.title))
class PromoteReport(Templated):
def __init__(self, links, link_text, bad_links, start, end):
self.links = links
self.start = start
self.end = end
if links:
self.make_link_report()
self.make_campaign_report()
p = request.get.copy()
self.csv_url = '%s.csv?%s' % (request.path, urlencode(p))
else:
self.link_report = None
self.campaign_report = None
self.csv_url = None
Templated.__init__(self, link_text=link_text, bad_links=bad_links)
def as_csv(self):
out = cStringIO.StringIO()
writer = csv.writer(out)
writer.writerow((_("start date"), self.start.strftime('%m/%d/%Y')))
writer.writerow((_("end date"), self.end.strftime('%m/%d/%Y')))
writer.writerow([])
writer.writerow((_("links"),))
writer.writerow((
_("name"),
_("owner"),
_("comments"),
_("upvotes"),
_("downvotes"),
))
for row in self.link_report:
writer.writerow((row['name'], row['owner'], row['comments'],
row['upvotes'], row['downvotes']))
writer.writerow([])
writer.writerow((_("campaigns"),))
writer.writerow((
_("link"),
_("owner"),
_("campaign"),
_("target"),
_("bid"),
_("frontpage clicks"), _("frontpage impressions"),
_("subreddit clicks"), _("subreddit impressions"),
_("total clicks"), _("total impressions"),
))
for row in self.campaign_report:
writer.writerow(
(row['link'], row['owner'], row['campaign'], row['target'],
row['bid'], row['fp_clicks'], row['fp_impressions'],
row['sr_clicks'], row['sr_impressions'], row['total_clicks'],
row['total_impressions'])
)
return out.getvalue()
def make_link_report(self):
link_report = []
owners = Account._byID([link.author_id for link in self.links],
data=True)
for link in self.links:
row = {
'name': link._fullname,
'owner': owners[link.author_id].name,
'comments': link.num_comments,
'upvotes': link._ups,
'downvotes': link._downs,
}
link_report.append(row)
self.link_report = link_report
@classmethod
def _get_hits(cls, traffic_cls, campaigns, start, end):
campaigns_by_name = {camp._fullname: camp for camp in campaigns}
codenames = campaigns_by_name.keys()
start = (start - promote.timezone_offset).replace(tzinfo=None)
end = (end - promote.timezone_offset).replace(tzinfo=None)
hits = traffic_cls.campaign_history(codenames, start, end)
sr_hits = defaultdict(int)
fp_hits = defaultdict(int)
for date, codename, sr, (uniques, pageviews) in hits:
campaign = campaigns_by_name[codename]
campaign_start = campaign.start_date - promote.timezone_offset
campaign_end = campaign.end_date - promote.timezone_offset
date = date.replace(tzinfo=g.tz)
if date < campaign_start or date > campaign_end:
continue
if sr == '':
fp_hits[codename] += pageviews
else:
sr_hits[codename] += pageviews
return fp_hits, sr_hits
@classmethod
def get_imps(cls, campaigns, start, end):
return cls._get_hits(traffic.TargetedImpressionsByCodename, campaigns,
start, end)
@classmethod
def get_clicks(cls, campaigns, start, end):
return cls._get_hits(traffic.TargetedClickthroughsByCodename, campaigns,
start, end)
def make_campaign_report(self):
campaigns = PromoCampaign._by_link([link._id for link in self.links])
def keep_camp(camp):
return not (camp.start_date.date() >= self.end.date() or
camp.end_date.date() <= self.start.date() or
not camp.trans_id)
campaigns = [camp for camp in campaigns if keep_camp(camp)]
fp_imps, sr_imps = self.get_imps(campaigns, self.start, self.end)
fp_clicks, sr_clicks = self.get_clicks(campaigns, self.start, self.end)
owners = Account._byID([link.author_id for link in self.links],
data=True)
links_by_id = {link._id: link for link in self.links}
campaign_report = []
for camp in campaigns:
link = links_by_id[camp.link_id]
fullname = camp._fullname
camp_duration = (camp.end_date - camp.start_date).days
effective_duration = (min(camp.end_date, self.end)
- max(camp.start_date, self.start)).days
bid = camp.bid * (float(effective_duration) / camp_duration)
row = {
'link': link._fullname,
'owner': owners[link.author_id].name,
'campaign': fullname,
'target': camp.sr_name or 'frontpage',
'bid': format_currency(bid, 'USD'),
'fp_impressions': fp_imps[fullname],
'sr_impressions': sr_imps[fullname],
'fp_clicks': fp_clicks[fullname],
'sr_clicks': sr_clicks[fullname],
'total_impressions': fp_imps[fullname] + sr_imps[fullname],
'total_clicks': fp_clicks[fullname] + sr_clicks[fullname],
}
campaign_report.append(row)
self.campaign_report = sorted(campaign_report, key=lambda r: r['link'])
class InnerToolbarFrame(Templated):
def __init__(self, link, expanded = False):
Templated.__init__(self, link = link, expanded = expanded)

View File

@@ -4853,6 +4853,46 @@ table.calendar {
border: none;
}
.promote-report-form {
margin: 1.5em 2em;
}
.promote-report-csv {
font-size: small;
}
.promote-report-table {
border: 0 none;
font-size: small;
margin: 1.5em 2em;
thead th {
font-weight: bold;
text-align: center;
padding: 0 1em;
border: 1px solid white;
background-color: #CEE3F8;
}
thead th.blank {
background: none;
}
thead th[colspan] {
text-align: center;
}
td {
text-align: right;
padding: 0 2em;
}
td.text {
text-align: left;
padding: 0 2em 0 0;
}
}
/* title box */
.titlebox {
font-size: larger;

View File

@@ -0,0 +1,153 @@
## The contents of this file are subject to the Common Public Attribution
## License Version 1.0. (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License at
## http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
## License Version 1.1, but Sections 14 and 15 have been added to cover use of
## software over a computer network and provide for limited attribution for the
## Original Developer. In addition, Exhibit A has been modified to be
## consistent with Exhibit B.
##
## Software distributed under the License is distributed on an "AS IS" basis,
## WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
## the specific language governing rights and limitations under the License.
##
## The Original Code is reddit.
##
## The Original Developer is the Initial Developer. The Initial Developer of
## the Original Code is reddit Inc.
##
## All portions of the code written by reddit are Copyright (c) 2006-2013
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib import js
from r2.lib.template_helpers import format_number
%>
<%namespace name="pr" file="promotelinkform.html" />
<%namespace file="utils.html" import="plain_link" />
${unsafe(js.use('sponsored'))}
<h1>sponsored link report</h1>
<div class="promote-report-form">
<form action="/promoted/report" method="get">
<h2>date range</h2>
<div class="note">
note: the end date is not included, so selecting 1/2/2013-1/3/2013 will retrieve traffic for the day of 1/2/2013 only.
</div>
<%pr:datepicker name="startdate" value="${thing.start.strftime('%m/%d/%Y')}"
minDateSrc="date-min" initfuncname="init_startdate"
min_date_offset="86400000">
function(elem) { check_enddate(elem, $("#enddate")); return elem; }
</%pr:datepicker>
&nbsp;&ndash;&nbsp;
<%pr:datepicker name="enddate" value="${thing.end.strftime('%m/%d/%Y')}"
minDateSrc="startdate" initfuncname="init_enddate"
min_date_offset="86400000">
function(elem) { return elem; }
</%pr:datepicker>
<h2>link names</h2>
<textarea name="link_text" rows="5" cols="80">
${thing.link_text}
</textarea>
%if thing.bad_links:
<div class="error">
${"%s not found" % (', '.join(thing.bad_links))}
</div>
%endif
<br>
<input type="submit" value="${_("go")}"></input>
</form>
</div>
%if thing.link_report:
<h1>links</h1>
<table id="link-report" class="promote-report-table">
<thead>
<tr>
<th>name</th>
<th>owner</th>
<th>comments</th>
<th>upvotes</th>
<th>downvotes</th>
</tr>
</thead>
<tbody>
%for row in thing.link_report:
<tr>
<td class="text">${row['name']}</td>
<td class="text">${row['owner']}</td>
<td>${format_number(row['comments'])}</td>
<td>${format_number(row['upvotes'])}</td>
<td>${format_number(row['downvotes'])}</td>
</tr>
%endfor
</tbody>
</table>
%endif
%if thing.campaign_report:
<h1>campaigns</h1>
<table id="campaign-report" class="promote-report-table">
<thead>
<tr>
<th class="blank"/>
<th class="blank"/>
<th class="blank"/>
<th class="blank"/>
<th class="blank"/>
<th colspan="2">frontpage</th>
<th colspan="2">subreddit</th>
<th colspan="2">total</th>
</tr>
<tr>
<th>link</th>
<th>owner</th>
<th>campaign</th>
<th>target</th>
<th>bid</th>
<th>clicks</th>
<th>impressions</th>
<th>clicks</th>
<th>impressions</th>
<th>clicks</th>
<th>impressions</th>
</tr>
</thead>
<tbody>
%for row in thing.campaign_report:
<tr>
<td class="text">${row['link']}</td>
<td class="text">${row['owner']}</td>
<td class="text">${row['campaign']}</td>
<td class="text">${row['target']}</td>
<td>${row['bid']}</td>
<td>${format_number(row['fp_clicks'])}</td>
<td>${format_number(row['fp_impressions'])}</td>
<td>${format_number(row['sr_clicks'])}</td>
<td>${format_number(row['sr_impressions'])}</td>
<td>${format_number(row['total_clicks'])}</td>
<td>${format_number(row['total_impressions'])}</td>
</tr>
%endfor
</tbody>
</table>
%endif
%if thing.csv_url:
<div class="promote-report-csv">
${plain_link(unsafe("download as csv"), thing.csv_url)}
</div>
%endif
<script type="text/javascript">
$(function() {
init_startdate();
init_enddate();
});
</script>