diff --git a/r2/r2/config/routing.py b/r2/r2/config/routing.py
index 51741c2fd..38f306189 100644
--- a/r2/r2/config/routing.py
+++ b/r2/r2/config/routing.py
@@ -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")
diff --git a/r2/r2/controllers/promotecontroller.py b/r2/r2/controllers/promotecontroller.py
index 1c8e43e38..ee2296ecd 100644
--- a/r2/r2/controllers/promotecontroller.py
+++ b/r2/r2/controllers/promotecontroller.py
@@ -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()
diff --git a/r2/r2/lib/pages/pages.py b/r2/r2/lib/pages/pages.py
index ebcad2cdc..85085be9b 100755
--- a/r2/r2/lib/pages/pages.py
+++ b/r2/r2/lib/pages/pages.py
@@ -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)
diff --git a/r2/r2/public/static/css/reddit.less b/r2/r2/public/static/css/reddit.less
index 921f1d088..1a3be95da 100755
--- a/r2/r2/public/static/css/reddit.less
+++ b/r2/r2/public/static/css/reddit.less
@@ -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;
diff --git a/r2/r2/templates/promotereport.html b/r2/r2/templates/promotereport.html
new file mode 100644
index 000000000..ae5ec8446
--- /dev/null
+++ b/r2/r2/templates/promotereport.html
@@ -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'))}
+
+
sponsored link report
+
+
+
+%if thing.link_report:
+links
+
+%endif
+
+%if thing.campaign_report:
+campaigns
+
+%endif
+
+%if thing.csv_url:
+
+ ${plain_link(unsafe("download as csv"), thing.csv_url)}
+
+%endif
+
+