mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-27 15:58:06 -05:00
PromoteReport for generating traffic reports on promoted links.
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
153
r2/r2/templates/promotereport.html
Normal file
153
r2/r2/templates/promotereport.html
Normal 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>
|
||||
–
|
||||
<%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>
|
||||
Reference in New Issue
Block a user