Overhaul traffic pages (and update a few self-serve pages).

* Upgrade flot and include the new timeseries flot plugin.
  (flot/flot@ca050b26c2)

* All times mentioned by traffic are now in UTC, not "local."

* Traffic data is generated as actual tables and JavaScript
  generates the Flot charts from the tables for accessibility.

* Many pieces of traffic data that were only accessible from the
  old traffic app are now moved into the reddit app.

* Traffic backend lag time is indicated on the graphs for clarity.

* Use excanvas with Flot instead of Google Charts for old-IE fallback.
This commit is contained in:
Neil Williams
2012-07-02 13:37:47 -07:00
parent 6a2c4afcb2
commit 94945f7341
32 changed files with 2774 additions and 1963 deletions

View File

@@ -61,19 +61,19 @@ clean_pyx:
#################### i18n
STRINGS_FILE := r2/lib/strings.py
RAND_STRINGS_FILE := r2/lib/rand_strings.py
GENERATED_STRINGS_FILE := r2/lib/generated_strings.py
POTFILE := $(I18NPATH)/r2.pot
.PHONY: i18n clean_i18n
i18n: $(RAND_STRINGS_FILE)
i18n: $(GENERATED_STRINGS_FILE)
$(PYTHON) setup.py extract_messages -o $(POTFILE)
$(RAND_STRINGS_FILE): $(STRINGS_FILE)
paster run standalone $(STRINGS_FILE) -c "print_rand_strings()" > $(RAND_STRINGS_FILE)
$(GENERATED_STRINGS_FILE): $(STRINGS_FILE)
paster run standalone $(STRINGS_FILE) -c "generate_strings()" > $(GENERATED_STRINGS_FILE)
clean_i18n:
rm -f $(RAND_STRINGS_FILE)
rm -f $(GENERATED_STRINGS_FILE)
#################### ini files
UPDATE_FILES := $(wildcard *.update)

View File

@@ -53,6 +53,8 @@ def make_map():
mc('/rules', controller='front', action='rules')
mc('/sup', controller='front', action='sup')
mc('/traffic', controller='front', action='site_traffic')
mc('/traffic/languages/:langcode', controller='front', action='lang_traffic', langcode='')
mc('/traffic/adverts/:code', controller='front', action='advert_traffic', code='')
mc('/account-activity', controller='front', action='account_activity')
mc('/about/message/:where', controller='message', action='listing')

View File

@@ -28,6 +28,7 @@ from r2.models import *
from r2.config.extensions import is_api
from r2.lib.pages import *
from r2.lib.pages.things import wrap_links
from r2.lib.pages import trafficpages
from r2.lib.menus import *
from r2.lib.utils import to36, sanitize_url, check_cheating, title_to_url
from r2.lib.utils import query_string, UrlParser, link_from_url, link_duplicates
@@ -584,8 +585,8 @@ class FrontController(RedditController):
pane = self._make_spamlisting(location, num, after, reverse, count)
if c.user.pref_private_feeds:
extension_handling = "private"
elif is_moderator and location == 'traffic':
pane = RedditTraffic()
elif (is_moderator or c.user_is_sponsor) and location == 'traffic':
pane = trafficpages.SubredditTraffic()
elif is_moderator and location == 'flair':
c.allow_styles = True
pane = FlairPane(num, after, reverse, name, user)
@@ -923,19 +924,27 @@ class FrontController(RedditController):
@validate(VTrafficViewer('article'),
article = VLink('article'))
def GET_traffic(self, article):
content = PromotedTraffic(article)
content = trafficpages.PromotedLinkTraffic(article)
if c.render_style == 'csv':
c.response.content = content.as_csv()
return c.response
return LinkInfoPage(link = article,
comment = None,
content = content).render()
return LinkInfoPage(link=article,
page_classes=["promoted-traffic"],
comment=None,
content=content).render()
@validate(VSponsorAdmin())
def GET_site_traffic(self):
return BoringPage("traffic",
content = RedditTraffic()).render()
return trafficpages.SitewideTrafficPage().render()
@validate(VSponsorAdmin())
def GET_lang_traffic(self, langcode):
return trafficpages.LanguageTrafficPage(langcode).render()
@validate(VSponsorAdmin())
def GET_advert_traffic(self, code):
return trafficpages.AdvertTrafficPage(code).render()
@validate(VUser())
def GET_account_activity(self):

View File

@@ -300,8 +300,19 @@ module["sponsored"] = Module("sponsored.js",
"sponsored.js"
)
module["flot"] = Module("jquery.flot.js",
"lib/jquery.flot.js"
module["timeseries"] = Module("timeseries.js",
"lib/jquery.flot.js",
"lib/jquery.flot.time.js",
"timeseries.js",
)
module["timeseries-ie"] = Module("timeseries-ie.js",
"lib/excanvas.min.js",
module["timeseries"],
)
module["traffic"] = LocalizedModule("traffic.js",
"traffic.js",
)
def use(*names):

View File

@@ -173,6 +173,10 @@ menu = MenuHandler(hot = _('hot'),
pending_promos = _('pending'),
rejected_promos = _('rejected'),
sitewide = _('sitewide'),
languages = _('languages'),
adverts = _('adverts'),
whitelist = _("whitelist")
)

View File

@@ -1,232 +0,0 @@
# 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-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
import math, datetime
def google_extended(n):
"""Computes the google extended encoding of an int in [0, 4096)"""
numerals = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789-.")
base = len(numerals)
assert(0 <= n <= base ** 2)
q, r = divmod(int(n), base)
return numerals[q] + numerals[r]
def make_date_axis_labels(series):
"""
assuming a uniform date series, generate a suitable axis label
"""
_max = max(series)
_min = min(series)
delta = _max - _min
zero = datetime.timedelta(0)
has_hour = isinstance(_min, datetime.datetime)
if delta != zero and has_hour and delta < datetime.timedelta(0, 0.5*86400):
test = lambda cur, prev: cur.hour != prev.hour and cur.hour % 3 == 0
format = "%H:00"
elif delta != zero and has_hour and delta < datetime.timedelta(2):
test = lambda cur, prev: cur.hour != prev.hour and cur.hour % 6 == 0
format = "%H:00"
elif delta == zero or delta < datetime.timedelta(7):
test = lambda cur, prev: cur.day != prev.day
format = "%d %b"
elif delta < datetime.timedelta(14):
test = lambda cur, prev: cur.day != prev.day and cur.day % 2 == 0
format = "%d %b"
elif delta < datetime.timedelta(30):
test = lambda cur, prev: (cur.day != prev.day) and cur.weekday() == 6
format = "%d %b"
else:
test = lambda cur, prev: (cur.month != prev.month)
format = "%b"
new_series = []
prev = None
for s in series:
if prev and test(s, prev):
new_series.append(s.strftime(format))
else:
new_series.append("")
prev = s
return new_series
class DataSeries(list):
def __init__(self, data):
list.__init__(self, data)
def low_precision_max(self, precision = 2):
"""
Compute the max of the data set, including at most 'precision'
units of decimal precision in the result (e.g., 9893 -> 9900 if
precision = 2)
"""
_max = float(max(self))
if _max == 0:
return 0
scale = math.log10(_max)
scale = 10 ** (math.ceil(scale) - precision)
return math.ceil(_max / scale) * scale
def normalize(self, norm_max = 100, precision = 2, _max = None):
_min = min(self)
_max = _max or max(self)
if _min == _max:
return DataSeries(int(norm_max)/2.
for i in xrange(len(self)))
else:
return DataSeries(min(int(x * float(norm_max) / _max), norm_max -1)
for x in self)
def toBarY(self):
data = []
for i in xrange(len(self)):
data += [self[i], self[i]]
return DataSeries(data)
def toBarX(self):
if len(self) > 1:
delta = self[-1] - self[-2]
else:
delta = 0
data = self.toBarY()
return DataSeries(data[1:] + [data[-1] + delta])
def is_regular(self):
return all(self[i] - self[i-1] == self[1] - self[0]
for i in xrange(1, len(self)))
def to_google_extended(self, precision = 1, _max = None):
if _max is None:
_max = self.low_precision_max(precision = precision)
norm_max = 4096
new = self.normalize(norm_max = norm_max, precision = precision,
_max = _max)
return _max, "".join(map(google_extended, new))
class LineGraph(object):
"""
General line chart class for plotting xy line graphs.
data is passed in as a series of tuples of the form (x, y_1, ...,
y_n) and converted to a single xdata DataSeries, and a list of
ydata DataSeries elements. The intention of this class is to be
able to handle multiple final plot representations, thought
currenly only google charts is available.
At some point, it also might make sense to connect this more
closely with numpy.
"""
google_api = "http://chart.apis.google.com/chart"
def __init__(self, xydata, colors = ("FF4500", "336699"),
width = 300, height = 175):
series = zip(*xydata)
self.xdata = DataSeries(series[0])
self.ydata = map(DataSeries, series[1:])
self.width = width
self.height = height
self.colors = colors
def google_chart(self, multiy = True, ylabels = [], title = "",
bar_fmt = True):
xdata, ydata = self.xdata, self.ydata
# Bar format makes the line chart look like it is a series of
# contiguous bars without the boundary line between each bar.
if bar_fmt:
xdata = DataSeries(range(len(self.xdata))).toBarX()
ydata = [y.toBarY() for y in self.ydata]
# TODO: currently we are only supporting time series. Make general
xaxis = make_date_axis_labels(self.xdata)
# Convert x data into google extended text format
xmax, xdata = xdata.to_google_extended(_max = max(xdata))
ymax0 = None
# multiy <=> 2 y axes with independent scaling. not multiy
# means we need to know what the global max is over all y data
multiy = multiy and len(ydata) == 2
if not multiy:
ymax0 = max(y.low_precision_max() for y in ydata)
def make_labels(i, m, p = 4):
return (("%d:|" % i) +
'|'.join(str(i * m / p)
for i in range(p+1)))
# data stores a list of xy data strings in google's format
data = []
labels = []
for i in range(len(ydata)):
ymax, y = ydata[i].to_google_extended(_max = ymax0)
data.append(xdata + ',' + y)
if multiy:
labels.append(make_labels(i,ymax))
if not multiy:
labels.append(make_labels(0,ymax0))
if multiy:
labels.append('2:|' + '|'.join(xaxis))
axes = 'y,r,x'
if len(self.colors) > 1:
ycolor = "0,%s|1,%s" % (self.colors[0], self.colors[1])
else:
ycolor = ""
else:
labels.append('1:|' + '|'.join(xaxis))
axes = 'y,x'
ycolor="",
if ylabels:
axes += ',t'
labels.append('%d:|' % (len(axes)/2) +
('|' if multiy else ', ').join(ylabels))
if title:
axes += ',t'
labels.append('%d:||%s|' % (len(axes)/2, title))
if len(self.colors) >= len(self.ydata):
colors = ",".join(self.colors[:len(self.ydata)])
else:
colors = ""
args = dict(# chart type is xy
cht = 'lxy',
# chart size
chs = "%sx%s" % (self.width, self.height),
# which axes are labeled
chxt= axes,
# axis labels
chxl = '|'.join(labels),
# chart data is in extended format
chd = 'e:' + ','.join(data),
chco = colors,
chxs = ycolor
)
return (self.google_api +
'?' + '&'.join('%s=%s' % (k, v) for k, v in args.iteritems()))

View File

@@ -28,6 +28,7 @@ from r2.models import Link, Printable, Trophy, bidding, PromotionWeights, Commen
from r2.models import Flair, FlairTemplate, FlairTemplateBySubredditIndex
from r2.models import USER_FLAIR, LINK_FLAIR
from r2.models.oauth2 import OAuth2Client
from r2.models import traffic
from r2.models import ModAction
from r2.models import Thing
from r2.config import cache
@@ -40,7 +41,6 @@ from pylons import c, request, g
from pylons.controllers.util import abort
from r2.lib import promote
from r2.lib.traffic import load_traffic, load_summary
from r2.lib.captcha import get_iden
from r2.lib.filters import spaceCompress, _force_unicode, _force_utf8
from r2.lib.filters import unsafe, websafe, SC_ON, SC_OFF, websafe_json
@@ -49,7 +49,7 @@ from r2.lib.menus import SubredditButton, SubredditMenu, ModeratorMailButton
from r2.lib.menus import OffsiteButton, menu, JsNavMenu
from r2.lib.strings import plurals, rand_strings, strings, Score
from r2.lib.utils import title_to_url, query_string, UrlParser, to_js, vote_hash
from r2.lib.utils import link_duplicates, make_offset_date, to_csv, median, to36
from r2.lib.utils import link_duplicates, make_offset_date, median, to36
from r2.lib.utils import trunc_time, timesince, timeuntil
from r2.lib.template_helpers import add_sr, get_domain, format_number
from r2.lib.subreddit_search import popular_searches
@@ -60,7 +60,7 @@ from r2.lib.utils import trunc_string as _truncate
from r2.lib.filters import safemarkdown
import sys, random, datetime, calendar, simplejson, re, time
import graph, pycountry, time
import pycountry, time
from itertools import chain
from urllib import quote
@@ -2896,32 +2896,6 @@ class BannedList(UserList):
def user_ids(self):
return c.site.banned
class TrafficViewerList(UserList):
"""Traffic share list on /traffic/*"""
destination = "traffic_viewer"
remove_action = "rm_traffic_viewer"
type = 'traffic'
def __init__(self, link, editable = True):
self.link = link
UserList.__init__(self, editable = editable)
@property
def form_title(self):
return _('share traffic')
@property
def table_title(self):
return _('current viewers')
def user_ids(self):
return promote.traffic_viewers(self.link)
@property
def container_name(self):
return self.link._fullname
class DetailsPage(LinkInfoPage):
extension_handling= False
@@ -3042,7 +3016,8 @@ class PromoteLinkForm(Templated):
self.now = promote.promo_datetime_now().date()
start_date = promote.promo_datetime_now(offset = -14).date()
end_date = promote.promo_datetime_now(offset = 14).date()
self.promo_traffic = dict(load_traffic('day', 'promos'))
self.promo_traffic = dict(promote.traffic_totals())
self.market, self.promo_counter = \
Promote_Graph.get_market(None, start_date, end_date)
@@ -3226,270 +3201,6 @@ class MediaEmbedBody(CachedTemplate):
res = CachedTemplate.render(self, *a, **kw)
return responsive(res, True)
class Traffic(Templated):
@staticmethod
def slice_traffic(traffic, *indices):
return [[a] + [b[i] for i in indices] for a, b in traffic]
class PromotedTraffic(Traffic):
"""
Traffic page for a promoted link, including 2 graphs (one for
impressions and one for clicks with uniques on each plotted in
multiy format) and a table of the data.
"""
def __init__(self, thing):
# TODO: needs a fix for multiple campaigns
self.thing = thing
d = until = None
self.traffic = []
if thing.campaigns:
d = min(sd.date() if isinstance(sd, datetime.datetime) else sd
for sd, ed, bid, sr, trans_id in thing.campaigns.values()
if trans_id)
until = max(ed.date() if isinstance(ed, datetime.datetime) else ed
for sd, ed, bid, sr, trans_id in thing.campaigns.values()
if trans_id)
now = datetime.datetime.now(g.tz).date()
# the results are preliminary until 1 day after the promotion ends
self.preliminary = (until + datetime.timedelta(1) > now)
self.traffic = load_traffic('hour', "thing", thing._fullname,
start_time = d, stop_time = until)
# TODO: ditch uniques and just sum the hourly values
self.totals = load_traffic('day', "thing", thing._fullname)
# generate a list of
# (uniq impressions, # impressions, uniq clicks, # clicks)
if self.totals:
self.totals = map(sum, zip(*zip(*self.totals)[1]))
imp = self.slice_traffic(self.traffic, 0, 1)
if len(imp) > 2:
imp_total = sum(x[2] for x in imp)
if self.totals:
self.totals[1] = max(self.totals[1], imp_total)
imp_total = format_number(imp_total)
self.imp_graph = TrafficGraph(imp[-72:], ylabels = ['uniques', 'total'],
title = ("recent impressions (%s total)" %
imp_total))
cli = self.slice_traffic(self.traffic, 2, 3)
cli_total = sum(x[2] for x in cli)
if self.totals:
self.totals[3] = max(self.totals[3], cli_total)
cli_total = format_number(cli_total)
self.cli_graph = TrafficGraph(cli[-72:], ylabels = ['uniques', 'total'],
title = ("recent clicks (%s total)" %
cli_total))
else:
self.imp_graph = self.cli_graph = None
editable = c.user_is_sponsor or c.user._id == thing.author_id
self.viewers = TrafficViewerList(thing, editable = editable)
Templated.__init__(self)
def to_iter(self, localize = True, total = False):
locale = c.locale
def num(x):
if localize:
return format_number(x, locale)
return str(x)
def row(label, data):
uimp, nimp, ucli, ncli = data
return (label,
num(uimp), num(nimp), num(ucli), num(ncli),
("%.2f%%" % (float(100*ucli) / uimp)) if uimp else "--.--%",
("%.2f%%" % (float(100*ncli) / nimp)) if nimp else "--.--%")
for date, data in self.traffic:
yield row(date.strftime("%Y-%m-%d %H:%M"), data)
if total:
yield row("total", self.totals)
def as_csv(self):
return to_csv(self.to_iter(localize = False, total = True))
class RedditTraffic(Traffic):
"""
fetches hourly and daily traffic for the current reddit. If the
current reddit is a default subreddit, fetches the site-wide
uniques and includes monthly totals. In this latter case, getter
methods are available for computing breakdown of site trafffic by
reddit.
"""
def __init__(self):
self.has_data = False
ivals = ["hour", "day", "month"]
for ival in ivals:
if c.default_sr:
data = load_traffic(ival, "total", "")
else:
data = load_traffic(ival, "reddit", c.site.name)
if not data:
break
slices = [("uniques", (0, 2) if c.site.domain else (0,),
"FF4500"),
("impressions", (1, 3) if c.site.domain else (1,),
"336699")]
if not c.default_sr and ival == 'day':
slices.append(("subscriptions", (4,), "00FF00"))
setattr(self, ival + "_data", data)
for name, indx, color in slices:
data2 = self.slice_traffic(data, *indx)
setattr(self, name + "_" + ival + "_chart", data2)
title = "%s by %s" % (name, ival)
res = TrafficGraph(data2, colors = [color], title = title)
setattr(self, name + "_" + ival, res)
else:
self.has_data = True
if self.has_data:
imp_by_day = [[] for i in range(7)]
uni_by_day = [[] for i in range(7)]
if c.site.domain:
dates, imps, foo = zip(*self.impressions_day_chart)
dates, uniques, foo = zip(*self.uniques_day_chart)
else:
dates, imps = zip(*self.impressions_day_chart)
dates, uniques = zip(*self.uniques_day_chart)
self.uniques_mean = sum(map(float, uniques))/len(uniques)
self.impressions_mean = sum(map(float, imps))/len(imps)
for i, d in enumerate(dates):
imp_by_day[d.weekday()].append(float(imps[i]))
uni_by_day[d.weekday()].append(float(uniques[i]))
self.uniques_by_dow = [sum(x)/max(len(x),1)
for x in uni_by_day]
self.impressions_by_dow = [sum(x)/max(len(x),1)
for x in imp_by_day]
Templated.__init__(self)
def reddits_summary(self):
if c.default_sr:
data = map(list, load_summary("reddit"))
data.sort(key = lambda x: x[1][1], reverse = True)
for d in data:
name = d[0]
for sr in (Friends, All, Sub, DefaultSR()):
if name == sr.name:
name = sr
break
else:
try:
name = Subreddit._by_name(name)
except NotFound:
name = DomainSR(name)
d[0] = name
return data
return res
def monthly_summary(self):
"""
Convenience method b/c it is bad form to do this much math
inside of a template.b
"""
res = []
if c.default_sr:
data = self.month_data
# figure out the mean number of users last month,
# unless today is the first and there is no data
days = self.day_data
now = datetime.datetime.utcnow()
if now.day != 1:
# project based on traffic so far
# totals are going to be up to yesterday
month_len = calendar.monthrange(now.year, now.month)[1]
lastmonth = datetime.datetime.utcnow().month
lastmonthyear = datetime.datetime.utcnow().year
if lastmonth == 1:
lastmonthyear -= 1
lastmonth = 1
else:
lastmonth = (lastmonth - 1) if lastmonth != 1 else 12
# length of last month
lastmonthlen = calendar.monthrange(lastmonthyear, lastmonth)[1]
lastdays = filter(lambda x: x[0].month == lastmonth, days)
thisdays = filter(lambda x: x[0].month == now.month, days)
user_scale = 0
if lastdays:
last_mean = (sum(u for (d, (u, v)) in lastdays) /
float(len(lastdays)))
day_mean = (sum(u for (d, (u, v)) in thisdays) /
float(len(thisdays)))
if last_mean and day_mean:
user_scale = ( (day_mean * month_len) /
(last_mean * lastmonthlen) )
last_month_users = 0
locale = c.locale
for x, (date, d) in enumerate(data):
res.append([("date", date.strftime("%Y-%m")),
("", format_number(d[0], locale)),
("", format_number(d[1], locale))])
last_d = data[x-1][1] if x else None
for i in range(2):
# store last month's users for this month's projection
if x == len(data) - 2 and i == 0:
last_month_users = d[i]
if x == 0:
res[-1].append(("",""))
# project, unless today is the first of the month
elif x == len(data) - 1 and now.day != 1:
# yesterday
yday = (datetime.datetime.utcnow()
-datetime.timedelta(1)).day
if i == 0:
scaled = int(last_month_users * user_scale)
else:
scaled = float(d[i] * month_len) / yday
res[-1].append(("gray",
format_number(scaled, locale)))
elif last_d and d[i] and last_d[i]:
f = 100 * (float(d[i])/last_d[i] - 1)
res[-1].append(("up" if f > 0 else "down",
"%5.2f%%" % f))
return res
class TrafficGraph(Templated):
def __init__(self, data, width = 300, height = 175,
bar_fmt = True, colors = ("FF4500", "336699"), title = '',
ylabels = [], multiy = True):
# fallback on google charts
chart = graph.LineGraph(data[:72], colors = colors)
self.gc = chart.google_chart(ylabels = ylabels, multiy = multiy, title = title)
xdata = []
ydata = []
for d in data:
xdata.append(time.mktime(d[0].timetuple())*1000)
ydata.append(d[1:])
ydata = zip(*ydata)
self.colors = colors
self.title = title
if bar_fmt:
xdata = graph.DataSeries(xdata).toBarX()
if ydata and not isinstance(ydata[0], (list, tuple)):
if bar_fmt:
ydata = graph.DataSeries(ydata).toBarY()
self.data = [zip(xdata, ydata)]
else:
self.data = []
for ys in ydata:
if bar_fmt:
ys = graph.DataSeries(ys).toBarY()
self.data.append(zip(xdata, ys))
self.width = width
self.height = height
Templated.__init__(self)
class RedditAds(Templated):
def __init__(self, **kw):
self.sr_name = c.site.name
@@ -3574,6 +3285,10 @@ class Promotion_Summary(Templated):
% ndays, p.render('email'))
def force_datetime(d):
return datetime.datetime.combine(d, datetime.time())
class Promote_Graph(Templated):
@classmethod
@@ -3665,84 +3380,43 @@ class Promote_Graph(Templated):
else:
break
# load recent traffic as well:
self.recent = {}
#TODO
for k, v in []:#load_summary("thing"):
if k.startswith('t%d_' % Link._type_id):
self.recent[k] = v
if self.recent:
link_listing = wrap_links(self.recent.keys())
for t in link_listing:
self.recent[t._fullname].insert(0, t)
self.recent = self.recent.values()
self.recent.sort(key = lambda x: x[0]._date)
pool =PromotionWeights.bid_history(promote.promo_datetime_now(offset=-30),
promote.promo_datetime_now(offset=2))
if pool:
# we want to generate a stacked line graph, so store the
# bids and the total including refunded amounts
total_sale = sum((b-r) for (d, b, r) in pool)
total_refund = sum(r for (d, b, r) in pool)
self.money_graph = TrafficGraph([(d, b, r) for (d, b, r) in pool],
colors = ("008800", "FF0000"),
ylabels = ['total ($)'],
title = ("monthly sales ($%.2f charged, $%.2f credits)" %
(total_sale, total_refund)),
multiy = False)
#TODO
self.top_promoters = []
else:
self.money_graph = None
self.top_promoters = []
# graphs of impressions and clicks
self.promo_traffic = load_traffic('day', 'promos')
self.promo_traffic = promote.traffic_totals()
impressions = [(d, i) for (d, (i, k)) in self.promo_traffic]
pool = dict((d, b+r) for (d, b, r) in pool)
if impressions:
self.imp_graph = TrafficGraph(impressions, ylabels = ['total'],
title = "impressions")
clicks = [(d, k) for (d, (i, k)) in self.promo_traffic]
CPM = [(d, (pool.get(d, 0) * 1000. / i) if i else 0)
CPM = [(force_datetime(d), (pool.get(d, 0) * 1000. / i) if i else 0)
for (d, (i, k)) in self.promo_traffic if d in pool]
CPC = [(d, (100 * pool.get(d, 0) / k) if k else 0)
for (d, (i, k)) in self.promo_traffic if d in pool]
CTR = [(d, (100 * float(k) / i if i else 0))
for (d, (i, k)) in self.promo_traffic if d in pool]
self.cli_graph = TrafficGraph(clicks, ylabels = ['total'],
title = "clicks")
mean_CPM = sum(x[1] for x in CPM) * 1. / max(len(CPM), 1)
self.cpm_graph = TrafficGraph([(d, min(x, mean_CPM*2)) for d, x in CPM],
colors = ["336699"], ylabels = ['CPM ($)'],
title = "cost per 1k impressions " +
"($%.2f average)" % mean_CPM)
CPC = [(force_datetime(d), (100 * pool.get(d, 0) / k) if k else 0)
for (d, (i, k)) in self.promo_traffic if d in pool]
mean_CPC = sum(x[1] for x in CPC) * 1. / max(len(CPC), 1)
self.cpc_graph = TrafficGraph([(d, min(x, mean_CPC*2)) for d, x in CPC],
colors = ["336699"], ylabels = ['CPC ($0.01)'],
title = "cost per click " +
"($%.2f average)" % (mean_CPC/100.))
self.ctr_graph = TrafficGraph(CTR, colors = ["336699"], ylabels = ['CTR (%)'],
title = "click through rate")
cpm_title = _("cost per 1k impressions ($%(avg).2f average)") % dict(avg=mean_CPM)
cpc_title = _("cost per click ($%(avg).2f average)") % dict(avg=mean_CPC/100.)
data = traffic.zip_timeseries(((d, (min(v, mean_CPM * 2),)) for d, v in CPM),
((d, (min(v, mean_CPC * 2),)) for d, v in CPC))
from r2.lib.pages.trafficpages import COLORS # not top level because of * imports :(
self.performance_table = TimeSeriesChart("promote-graph-table",
_("historical performance"),
"day",
[dict(color=COLORS.DOWNVOTE_BLUE,
title=cpm_title,
shortname=_("CPM")),
dict(color=COLORS.DOWNVOTE_BLUE,
title=cpc_title,
shortname=_("CPC"))],
data)
else:
self.imp_graph = self.cli_graph = None
self.cpc_graph = self.cpm_graph = None
self.ctr_graph = None
self.performance_table = None
self.promo_traffic = dict(self.promo_traffic)
@@ -3767,9 +3441,6 @@ class Promote_Graph(Templated):
"$%.2f" % link.promote_bid,
_force_unicode(link.title))
def as_csv(self):
return to_csv(self.to_iter(localize = False))
class InnerToolbarFrame(Templated):
def __init__(self, link, expanded = False):
Templated.__init__(self, link = link, expanded = expanded)
@@ -3904,3 +3575,17 @@ class ApiHelp(Templated):
class RulesPage(Templated):
pass
class TimeSeriesChart(Templated):
def __init__(self, id, title, interval, columns, rows,
latest_available_data=None, classes=[]):
self.id = id
self.title = title
self.interval = interval
self.columns = columns
self.rows = rows
self.latest_available_data = (latest_available_data or
datetime.datetime.utcnow())
self.classes = " ".join(classes)
Templated.__init__(self)

View File

@@ -0,0 +1,430 @@
# 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-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
import datetime
from pylons.i18n import _
from pylons import g, c
import babel.core
from r2.lib.menus import menu
from r2.lib.wrapped import Templated
from r2.lib.pages.pages import Reddit, TimeSeriesChart, UserList
from r2.lib.menus import NavButton, NamedButton, PageNameNav, NavMenu
from r2.lib import promote
from r2.lib.utils import Storage
from r2.models import Thing, Link, traffic
from r2.models.subreddit import Subreddit, _DefaultSR
COLORS = Storage(UPVOTE_ORANGE="#ff5700",
DOWNVOTE_BLUE="#9494ff",
MISC_GREEN="#00aa00")
class TrafficPage(Reddit):
extension_handling = False
extra_page_classes = ["traffic"]
def __init__(self, content):
Reddit.__init__(self, title=_("traffic stats"), content=content)
def build_toolbars(self):
main_buttons = [NavButton(menu.sitewide, "/"),
NamedButton("languages"),
NamedButton("adverts")]
toolbar = [PageNameNav("nomenu", title=self.title),
NavMenu(main_buttons, base_path="/traffic", type="tabmenu")]
return toolbar
class SitewideTrafficPage(TrafficPage):
extra_page_classes = TrafficPage.extra_page_classes + ["traffic-sitewide"]
def __init__(self):
TrafficPage.__init__(self, SitewideTraffic())
class LanguageTrafficPage(TrafficPage):
def __init__(self, langcode):
if langcode:
content = LanguageTraffic(langcode)
else:
content = LanguageTrafficSummary()
TrafficPage.__init__(self, content)
class AdvertTrafficPage(TrafficPage):
def __init__(self, code):
if code:
content = AdvertTraffic(code)
else:
content = AdvertTrafficSummary()
TrafficPage.__init__(self, content)
class RedditTraffic(Templated):
def __init__(self, place):
self.place = place
self.traffic_last_modified = traffic.get_traffic_last_modified()
self.traffic_lag = (datetime.datetime.utcnow() -
self.traffic_last_modified)
self.get_tables()
Templated.__init__(self)
def get_tables(self):
self.tables = []
for interval in ("month", "day", "hour"):
columns = [
dict(color=COLORS.UPVOTE_ORANGE,
title=_("uniques by %s" % interval),
shortname=_("uniques")),
dict(color=COLORS.DOWNVOTE_BLUE,
title=_("pageviews by %s" % interval),
shortname=_("pageviews")),
]
data = self.get_data_for_interval(interval, columns)
title = _("traffic by %s" % interval)
graph = TimeSeriesChart("traffic-" + interval,
title,
interval,
columns,
data,
self.traffic_last_modified,
classes=["traffic-table"])
self.tables.append(graph)
try:
self.dow_summary = self.get_dow_summary()
except NotImplementedError:
self.dow_summary = None
else:
self.dow_summary = self.dow_summary[1:8] # latest complete week
mean_uniques = sum(r[1][0] for r in self.dow_summary) / 7.0
mean_pageviews = sum(r[1][1] for r in self.dow_summary) / 7.0
self.dow_means = (round(mean_uniques), round(mean_pageviews))
def get_dow_summary(self):
raise NotImplementedError()
def get_data_for_interval(self, interval, columns):
raise NotImplementedError()
class SitewideTraffic(RedditTraffic):
def __init__(self):
subreddit_summary = traffic.PageviewsBySubreddit.top_last_month()
self.subreddit_summary = []
for srname, data in subreddit_summary:
if srname == _DefaultSR.name:
name = _("[frontpage]")
url = None
elif srname in Subreddit._specials:
name = "[%s]" % srname
url = None
else:
name = "/r/%s" % srname
url = name + "/about/traffic"
self.subreddit_summary.append(((name, url), data))
RedditTraffic.__init__(self, g.domain)
def get_dow_summary(self):
return traffic.SitewidePageviews.history("day")
def get_data_for_interval(self, interval, columns):
return traffic.SitewidePageviews.history(interval)
class LanguageTrafficSummary(Templated):
def __init__(self):
# convert language codes to real names
language_summary = traffic.PageviewsByLanguage.top_last_month()
locale = c.locale
self.language_summary = []
for language_code, data in language_summary:
name = LanguageTraffic.get_language_name(language_code, locale)
self.language_summary.append(((language_code, name), data))
Templated.__init__(self)
class AdvertTrafficSummary(RedditTraffic):
def __init__(self):
RedditTraffic.__init__(self, _("adverts"))
def get_tables(self):
# overall promoted link traffic
impressions = traffic.AdImpressionsByCodename.historical_totals("day")
clicks = traffic.ClickthroughsByCodename.historical_totals("day")
data = traffic.zip_timeseries(impressions, clicks)
columns = [
dict(color=COLORS.UPVOTE_ORANGE,
title=_("total impressions by day"),
shortname=_("impressions")),
dict(color=COLORS.DOWNVOTE_BLUE,
title=_("total clicks by day"),
shortname=_("clicks")),
]
self.totals = TimeSeriesChart("traffic-ad-totals",
_("ad totals"),
"day",
columns,
data,
self.traffic_last_modified,
classes=["traffic-table"])
# get summary of top ads
advert_summary = traffic.AdImpressionsByCodename.top_last_month()
things = AdvertTrafficSummary.get_things(ad for ad, data
in advert_summary)
self.advert_summary = []
for id, data in advert_summary:
name = AdvertTrafficSummary.get_ad_name(id, things=things)
url = AdvertTrafficSummary.get_ad_url(id, things=things)
self.advert_summary.append(((name, url), data))
@staticmethod
def get_things(codes):
fullnames = [code for code in codes
if code.startswith(Thing._type_prefix)]
return Thing._by_fullname(fullnames, data=True, return_dict=True)
@staticmethod
def get_sr_name(name):
if name == g.default_sr:
return _("frontpage")
else:
return "/r/" + name
@staticmethod
def get_ad_name(code, things=None):
if not things:
things = AdvertTrafficSummary.get_things([code])
thing = things.get(code)
if not thing:
if code.startswith("dart_"):
srname = code.split("_", 1)[1]
srname = AdvertTrafficSummary.get_sr_name(srname)
return "DART: " + srname
else:
return code
elif isinstance(thing, Link):
return "Link: " + thing.title
elif isinstance(thing, Subreddit):
srname = AdvertTrafficSummary.get_sr_name(thing.name)
return "300x100: " + srname
@staticmethod
def get_ad_url(code, things):
thing = things.get(code)
if isinstance(thing, Link):
return "/traffic/%s" % thing._id36
return "/traffic/adverts/%s" % code
class LanguageTraffic(RedditTraffic):
def __init__(self, langcode):
self.langcode = langcode
name = LanguageTraffic.get_language_name(langcode)
RedditTraffic.__init__(self, name)
def get_data_for_interval(self, interval, columns):
return traffic.PageviewsByLanguage.history(interval, self.langcode)
@staticmethod
def get_language_name(language_code, locale=None):
if not locale:
locale = c.locale
try:
lang_locale = babel.core.Locale.parse(language_code, sep="-")
except (babel.core.UnknownLocaleError, ValueError):
return language_code
else:
return lang_locale.get_display_name(locale)
class AdvertTraffic(RedditTraffic):
def __init__(self, code):
self.code = code
name = AdvertTrafficSummary.get_ad_name(code)
RedditTraffic.__init__(self, name)
def get_data_for_interval(self, interval, columns):
columns[1]["title"] = _("impressions by %s" % interval)
columns[1]["shortname"] = _("impressions")
columns += [
dict(shortname=_("unique clicks")),
dict(color=COLORS.MISC_GREEN,
title=_("clicks by %s" % interval),
shortname=_("total clicks")),
]
imps = traffic.AdImpressionsByCodename.history(interval, self.code)
clicks = traffic.ClickthroughsByCodename.history(interval, self.code)
return traffic.zip_timeseries(imps, clicks)
class SubredditTraffic(RedditTraffic):
def __init__(self):
RedditTraffic.__init__(self, "/r/" + c.site.name)
def get_dow_summary(self):
return traffic.PageviewsBySubreddit.history("day", c.site.name)
def get_data_for_interval(self, interval, columns):
pageviews = traffic.PageviewsBySubreddit.history(interval, c.site.name)
if interval == "day":
columns.append(dict(color=COLORS.MISC_GREEN,
title=_("subscriptions by day"),
shortname=_("subscriptions")))
sr_name = c.site.name
subscriptions = traffic.SubscriptionsBySubreddit.history(interval,
sr_name)
return traffic.zip_timeseries(pageviews, subscriptions)
else:
return pageviews
class PromotedLinkTraffic(RedditTraffic):
def __init__(self, thing):
self.thing = thing
editable = c.user_is_sponsor or c.user._id == thing.author_id
self.viewer_list = TrafficViewerList(thing, editable)
RedditTraffic.__init__(self, None)
@staticmethod
def calculate_clickthrough_rate(impressions, clicks):
if impressions:
return (float(clicks) / impressions) * 100.
else:
return 0
def get_tables(self):
start, end = promote.get_total_run(self.thing)
if not start or not end:
self.history = []
return
fullname = self.thing._fullname
imps = traffic.AdImpressionsByCodename.promotion_history(fullname,
start, end)
clicks = traffic.ClickthroughsByCodename.promotion_history(fullname,
start, end)
history = traffic.zip_timeseries(imps, clicks)
computed_history = []
self.total_impressions, self.total_clicks = 0, 0
for date, data in history:
u_imps, imps, u_clicks, clicks = data
u_ctr = self.calculate_clickthrough_rate(u_imps, u_clicks)
ctr = self.calculate_clickthrough_rate(imps, clicks)
self.total_impressions += imps
self.total_clicks += clicks
computed_history.append((date, data + (u_ctr, ctr)))
self.history = computed_history
if self.total_impressions > 0:
self.total_ctr = ((float(self.total_clicks) /
self.total_impressions) * 100.)
# the results are preliminary until 1 day after the promotion ends
now = datetime.datetime.utcnow()
self.is_preliminary = end + datetime.timedelta(days=1) > now
# 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):
import csv
import cStringIO
out = cStringIO.StringIO()
writer = csv.writer(out)
history = self.get_tables()
writer.writerow((_("date and time (UTC)"),
_("unique impressions"),
_("total impressions"),
_("unique clicks"),
_("total clicks"),
_("unique click-through rate (%)"),
_("total click-through rate (%)")))
for date, values in history:
# flatten (date, value-tuple) to (date, value1, value2...)
writer.writerow((date,) + values)
return out.getvalue()
class TrafficViewerList(UserList):
"""Traffic share list on /traffic/*"""
destination = "traffic_viewer"
remove_action = "rm_traffic_viewer"
type = "traffic"
def __init__(self, link, editable=True):
self.link = link
UserList.__init__(self, editable=editable)
@property
def form_title(self):
return _("share traffic")
@property
def table_title(self):
return _("current viewers")
def user_ids(self):
return promote.traffic_viewers(self.link)
@property
def container_name(self):
return self.link._fullname

View File

@@ -316,6 +316,13 @@ def rm_traffic_viewer(thing, user):
def traffic_viewers(thing):
return sorted(getattr(thing, "promo_traffic_viewers", set()))
def traffic_totals():
from r2.models import traffic
impressions = traffic.AdImpressionsByCodename.historical_totals("day")
clicks = traffic.ClickthroughsByCodename.historical_totals("day")
traffic_data = traffic.zip_timeseries(impressions, clicks)
return [(d.date(), v) for d, v in traffic_data]
# logging routine for keeping track of diffs
def promotion_log(thing, text, commit = False):
"""
@@ -662,7 +669,7 @@ def scheduled_campaigns_by_link(l, date=None):
return accepted
def get_traffic_weights(srnames):
from r2.lib import traffic
from r2.models.traffic import PageviewsBySubreddit
# the weight is just the last 7 days of impressions (averaged)
def weigh(t, npoints = 7):
@@ -671,7 +678,7 @@ def get_traffic_weights(srnames):
return max(float(sum(t)) / len(t), 1)
return 1
default_traffic = [weigh(traffic.load_traffic("day", "reddit", sr.name))
default_traffic = [weigh(PageviewsBySubreddit.history("day", sr.name))
for sr in Subreddit.top_lang_srs('all', 10)]
default_traffic = (float(max(sum(default_traffic),1)) /
max(len(default_traffic), 1))
@@ -680,7 +687,7 @@ def get_traffic_weights(srnames):
for srname in srnames:
if srname:
res[srname] = (default_traffic /
weigh(traffic.load_traffic("day", "reddit", srname)) )
weigh(PageviewsBySubreddit.history("day", sr.name)) )
else:
res[srname] = 1
return res
@@ -881,6 +888,34 @@ def benchmark_promoted(user, site, pos = 0, link_sample = 50, attempts = 100):
print "%s: %5.3f %3.5f" % (l,float(v)/attempts, expected.get(l, 0))
def get_total_run(link):
"""Return the total time span this promotion has run for.
Starts at the start date of the earliest campaign and goes to the end date
of the latest campaign.
"""
if not link.campaigns:
return None, None
earliest = None
latest = None
for start, end, bid, sr, trans_id in link.campaigns.itervalues():
if not trans_id:
continue
if not earliest or start < earliest:
earliest = start
if not latest or end > latest:
latest = end
# ugh this stuff is a mess. they're stored as "UTC" but actually mean UTC-5.
earliest = earliest.replace(tzinfo=None) - timezone_offset
latest = latest.replace(tzinfo=None) - timezone_offset
return earliest, latest
def Run(offset = 0):
charge_pending(offset = offset + 1)

View File

@@ -171,6 +171,23 @@ string_dict = dict(
account_activity_blurb = _("This page shows a history of recent activity on your account. If you notice unusual activity, you should change your password immediately. Location information is guessed from your computer's IP address and may be wildly wrong, especially for visits from mobile devices. Note: due to a bug, private-use addresses (starting with 10.) sometimes show up erroneously in this list after regular use of the site."),
your_current_ip_is = _("You are currently accessing reddit from this IP address: %(address)s."),
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_subreddit_explanation = _("""
Below are the traffic statistics for your subreddit. Each graph represents one of the following over the interval specified.
* **pageviews** are all hits to %(subreddit)s, including both listing pages and comment pages.
* **uniques** are the total number of unique visitors (determined by a combination of their IP address and User Agent string) that generate the above pageviews. This is independent of whether or not they are logged in.
* **subscriptions** is the number of new subscriptions that have been generated in a given day. This number is less accurate than the first two metrics, as, though we can track new subscriptions, we have no way to track unsubscriptions.
Note: there are a couple of places outside of your subreddit where someone can click "subscribe", so it is possible (though unlikely) that the subscription count can exceed the unique count on a given day.
"""),
go = _("go"),
view_subreddit_traffic = _("view subreddit traffic"),
)
class StringHandler(object):
@@ -393,8 +410,18 @@ rand_strings.add('sadmessages', "Funny 500 page message", 10)
rand_strings.add('create_reddit', "Reason to create a reddit", 20)
def print_rand_strings():
def generate_strings():
"""Print out automatically generated strings for translation."""
# used by error pages and in the sidebar for why to create a subreddit
for name, rand_string in rand_strings:
for string in rand_string:
print "# TRANSLATORS: Do not translate literally. Come up with a funny/relevant phrase (see the English version for ideas)"
print "print _('" + string + "')"
# these are used in r2.lib.pages.trafficpages
INTERVALS = ("hour", "day", "month")
TYPES = ("uniques", "pageviews", "traffic", "impressions", "clicks")
for interval in INTERVALS:
for type in TYPES:
print "print _('%s by %s')" % (type, interval)

View File

@@ -33,6 +33,7 @@ import os.path
from copy import copy
import random
import urlparse
import calendar
from pylons import g, c
from pylons.i18n import _, ungettext
from paste.util.mimeparse import desired_matches
@@ -530,3 +531,7 @@ def format_number(number, locale=None):
locale = c.locale
return babel.numbers.format_number(number, locale=locale)
def js_timestamp(date):
return '%d' % (calendar.timegm(date.timetuple()) * 1000)

View File

@@ -1,92 +0,0 @@
# 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-2012 reddit
# Inc. All Rights Reserved.
###############################################################################
import time
import datetime
from r2.lib import promote
from r2.models import traffic
def force_datetime(dt):
if isinstance(dt, datetime.datetime):
return dt
elif isinstance(dt, datetime.date):
return datetime.datetime.combine(dt, datetime.time())
else:
raise NotImplementedError()
def load_traffic(interval, what, iden="",
start_time=None, stop_time=None,
npoints=None):
if what == "reddit":
sr_traffic = traffic.PageviewsBySubreddit.history(interval, iden)
# add in null values for cname stuff
res = [(t, v + (0, 0)) for (t, v) in sr_traffic]
# day interval needs subscription numbers
if interval == "day":
subscriptions = traffic.SubscriptionsBySubreddit.history(interval,
iden)
res = traffic.zip_timeseries(res, subscriptions)
elif what == "total":
res = traffic.SitewidePageviews.history(interval)
elif what == "summary" and iden == "reddit" and interval == "month":
sr_traffic = traffic.PageviewsBySubreddit.top_last_month()
# add in null values for cname stuff
# return directly because this doesn't have a date parameter first
return [(t, v + (0, 0)) for (t, v) in sr_traffic]
elif what == "promos" and interval == "day":
pageviews = traffic.AdImpressionsByCodename.historical_totals(interval)
clicks = traffic.ClickthroughsByCodename.historical_totals(interval)
res = traffic.zip_timeseries(pageviews, clicks)
elif what == "thing" and interval == "hour" and start_time:
start_time = force_datetime(start_time) - promote.timezone_offset
stop_time = force_datetime(stop_time) - promote.timezone_offset
pageviews = traffic.AdImpressionsByCodename.promotion_history(iden,
start_time,
stop_time)
clicks = traffic.ClickthroughsByCodename.promotion_history(iden,
start_time,
stop_time)
res = traffic.zip_timeseries(pageviews, clicks)
elif what == "thing" and not start_time:
pageviews = traffic.AdImpressionsByCodename.history(interval, iden)
clicks = traffic.ClickthroughsByCodename.history(interval, iden)
res = traffic.zip_timeseries(pageviews, clicks)
else:
raise NotImplementedError()
if interval == "hour":
# convert to local time
tzoffset = datetime.timedelta(0, time.timezone)
res = [(d - tzoffset, v) for d, v in res]
else:
res = [(d.date(), v) for d, v in res]
return res
def load_summary(what, interval = "month", npoints = 50):
return load_traffic(interval, "summary", what, npoints = npoints)

View File

@@ -1097,15 +1097,6 @@ def make_offset_date(start_date, interval, future = True,
return start_date - timedelta(interval)
return start_date
def to_csv(table):
# commas and linebreaks must result in a quoted string
def quote_commas(x):
if ',' in x or '\n' in x:
return u'"%s"' % x.replace('"', '""')
return x
return u"\n".join(u','.join(quote_commas(y) for y in x)
for x in table)
def in_chunks(it, size=25):
chunk = []
it = iter(it)

View File

@@ -189,6 +189,13 @@ def time_range(interval):
return start_time, stop_time
def points_for_interval(interval):
"""Calculate the number of data points to render for a given interval."""
range = time_range_by_interval[interval]
interval = timedelta_by_name(interval)
return range.total_seconds() / interval.total_seconds()
def make_history_query(cls, interval):
"""Build a generic query showing the history of a given aggregate."""

View File

@@ -3821,46 +3821,97 @@ ul.tabmenu.formtab {
}
/***traffic stuff***/
.traffic-table {margin: 10px 20px; }
.traffic-table,
.traffic-tables-side fieldset {
margin: 1.5em 2em;
font-size: small;
border: 0;
}
.traffic-table caption,
.traffic-tables-side fieldset legend{
font-weight: bold;
text-align: left;
font-size: medium;
font-variant: small-caps;
}
.traffic-table a:hover { text-decoration: underline; }
.traffic-table th { font-weight: bold; text-align: center;}
.traffic-table th,
.traffic-table thead th { font-weight: bold; text-align: center; padding-left: 2em;}
.traffic-table thead th:first-child { text-align: left; padding-left: 0; }
.traffic-table tbody th,
.traffic-table tfoot th { text-align: left;}
.traffic-table td { padding: 0 5px; }
.traffic-table td { text-align: right; }
.traffic-table td.up { color: #FF8B60; }
.traffic-table td.down { color: #336699; }
.traffic-table td.gray { color: gray; font-style: italic; }
.traffic-table tfoot tr { border-top: 1px solid black; }
.traffic-table tfoot th,
.traffic-table tfoot td { font-style: italic; }
.traffic-table tr.max { border: 2px solid #FF8B60; }
.traffic-table tr.min { border: 2px solid #336699; }
.traffic-table tr.odd { background-color: #E0E0E0; }
.traffic-table tbody tr:nth-child(even) { background-color: #E0E0E0; }
.traffic-table tr.mean { font-style: italic; border-top: 1px solid; }
.traffic-table .prelim { font-style: italic; }
.traffic-table .totals { font-style: italic; border-top: 1px solid black; }
.traffic-graph {
padding: 10px;
border: 1px solid #B0B0B0;
margin-left: 10px;
margin-bottom: 10px;
display: inline-block;
.traffic-tables-side {
float: left;
min-height: 50em;
}
.traffic-graph .title {
#promote-graph-table,
#traffic-hour {
display: none;
}
div.timeseries {
padding: 10px;
border: 1px solid #B0B0B0;
margin: 10px 10px;
display: inline-block;
text-align: center;
}
.timeseries-placeholder {
width: 350px;
height: 200px;
font-family: verdana;
font-size: small;
}
div.timeseries span.title {
font-weight: bold;
font-size: medium;
font-variant: small-caps;
}
.promoted-traffic h1 {
border: none;
margin-bottom: 10px;
#timeseries-unprocessed {
font-size: small;
font-weight: bold;
color: #900;
margin: 1em 0;
max-width: 60em;
}
.timeseries-tablebar {
height: 5px;
margin: 1px 0;
}
.promoted-traffic .usertable { margin-left: 0px; }
.promoted-traffic h1 a {
.promoted-traffic h1 a {
font-size: small;
margin-left: 10px;
margin-left: 10px;
}
.promoted-traffic tfoot th,
.promoted-traffic tfoot td {
font-style: normal;
font-weight: bold;
text-transform: uppercase;
padding-top: .3em;
}
p.totals-are-preliminary {
margin-left: 10px;
}
.award-square-container {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,308 @@
/*
Pretty handling of time axes.
Set axis.mode to "time" to enable. See the section "Time series data" in API.txt
for details.
*/
(function ($) {
var options = {};
// round to nearby lower multiple of base
function floorInBase(n, base) {
return base * Math.floor(n / base);
}
// Returns a string with the date d formatted according to fmt.
// A subset of the Open Group's strftime format is supported.
function formatDate(d, fmt, monthNames, dayNames) {
if (typeof d.strftime == "function") {
return d.strftime(fmt);
}
var leftPad = function(n, pad) {
n = "" + n;
pad = "" + (pad == null ? "0" : pad);
return n.length == 1 ? pad + n : n;
};
var r = [];
var escape = false;
var hours = d.getHours();
var isAM = hours < 12;
if (monthNames == null)
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
if (dayNames == null)
dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
var hours12;
if (hours > 12) {
hours12 = hours - 12;
} else if (hours == 0) {
hours12 = 12;
} else {
hours12 = hours;
}
for (var i = 0; i < fmt.length; ++i) {
var c = fmt.charAt(i);
if (escape) {
switch (c) {
case 'a': c = "" + dayNames[d.getDay()]; break;
case 'b': c = "" + monthNames[d.getMonth()]; break;
case 'd': c = leftPad(d.getDate()); break;
case 'e': c = leftPad(d.getDate(), " "); break;
case 'H': c = leftPad(hours); break;
case 'I': c = leftPad(hours12); break;
case 'l': c = leftPad(hours12, " "); break;
case 'm': c = leftPad(d.getMonth() + 1); break;
case 'M': c = leftPad(d.getMinutes()); break;
case 'S': c = leftPad(d.getSeconds()); break;
case 'y': c = leftPad(d.getFullYear() % 100); break;
case 'Y': c = "" + d.getFullYear(); break;
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
case 'w': c = "" + d.getDay(); break;
}
r.push(c);
escape = false;
}
else {
if (c == "%")
escape = true;
else
r.push(c);
}
}
return r.join("");
}
// To have a consistent view of time-based data independent of which time
// zone the client happens to be in we need a date-like object independent
// of time zones. This is done through a wrapper that only calls the UTC
// versions of the accessor methods.
function makeUtcWrapper(d) {
function addProxyMethod(sourceObj, sourceMethod, targetObj,
targetMethod) {
sourceObj[sourceMethod] = function() {
return targetObj[targetMethod].apply(targetObj, arguments);
};
};
var utc = {
date: d
};
// support strftime, if found
if (d.strftime != undefined)
addProxyMethod(utc, "strftime", d, "strftime");
addProxyMethod(utc, "getTime", d, "getTime");
addProxyMethod(utc, "setTime", d, "setTime");
var props = [ "Date", "Day", "FullYear", "Hours", "Milliseconds", "Minutes", "Month", "Seconds" ];
for (var p = 0; p < props.length; p++) {
addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
}
return utc;
};
// select time zone strategy. This returns a date-like object tied to the
// desired timezone
function dateGenerator(ts, opts) {
if (opts.timezone == "browser") {
return new Date(ts);
} else if (!opts.timezone || opts.timezone == "utc") {
return makeUtcWrapper(new Date(ts));
} else if (typeof timezoneJS != "undefined" && typeof timezoneJS.Date != "undefined") {
var d = new timezoneJS.Date();
// timezone-js is fickle, so be sure to set the time zone before
// setting the time.
d.setTimezone(opts.timezone);
d.setTime(ts);
return d;
} else {
return makeUtcWrapper(new Date(ts));
}
}
// map of app. size of time units in milliseconds
var timeUnitSize = {
"second": 1000,
"minute": 60 * 1000,
"hour": 60 * 60 * 1000,
"day": 24 * 60 * 60 * 1000,
"month": 30 * 24 * 60 * 60 * 1000,
"year": 365.2425 * 24 * 60 * 60 * 1000
};
// the allowed tick sizes, after 1 year we use
// an integer algorithm
var spec = [
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
[30, "second"],
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
[30, "minute"],
[1, "hour"], [2, "hour"], [4, "hour"],
[8, "hour"], [12, "hour"],
[1, "day"], [2, "day"], [3, "day"],
[0.25, "month"], [0.5, "month"], [1, "month"],
[2, "month"], [3, "month"], [6, "month"],
[1, "year"]
];
function init(plot) {
plot.hooks.processDatapoints.push(function (plot, series, datapoints) {
$.each(plot.getAxes(), function(axisName, axis) {
var opts = axis.options;
if (opts.mode == "time") {
axis.tickGenerator = function(axis) {
var ticks = [],
d = dateGenerator(axis.min, opts),
minSize = 0;
if (opts.minTickSize != null) {
if (typeof opts.tickSize == "number")
minSize = opts.tickSize;
else
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
}
for (var i = 0; i < spec.length - 1; ++i)
if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]]
+ spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
&& spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
break;
var size = spec[i][0];
var unit = spec[i][1];
// special-case the possibility of several years
if (unit == "year") {
// if given a minTickSize in years, just use it,
// ensuring that it's an integer
if (opts.minTickSize != null && opts.minTickSize[1] == "year") {
size = Math.floor(opts.minTickSize[0]);
} else {
var magn = Math.pow(10, Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
var norm = (axis.delta / timeUnitSize.year) / magn;
if (norm < 1.5)
size = 1;
else if (norm < 3)
size = 2;
else if (norm < 7.5)
size = 5;
else
size = 10;
size *= magn;
}
// minimum size for years is 1
if (size < 1)
size = 1;
}
axis.tickSize = opts.tickSize || [size, unit];
var tickSize = axis.tickSize[0];
unit = axis.tickSize[1];
var step = tickSize * timeUnitSize[unit];
if (unit == "second")
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
if (unit == "minute")
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
if (unit == "hour")
d.setHours(floorInBase(d.getHours(), tickSize));
if (unit == "month")
d.setMonth(floorInBase(d.getMonth(), tickSize));
if (unit == "year")
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
// reset smaller components
d.setMilliseconds(0);
if (step >= timeUnitSize.minute)
d.setSeconds(0);
if (step >= timeUnitSize.hour)
d.setMinutes(0);
if (step >= timeUnitSize.day)
d.setHours(0);
if (step >= timeUnitSize.day * 4)
d.setDate(1);
if (step >= timeUnitSize.year)
d.setMonth(0);
var carry = 0, v = Number.NaN, prev;
do {
prev = v;
v = d.getTime();
ticks.push(v);
if (unit == "month") {
if (tickSize < 1) {
// a bit complicated - we'll divide the month
// up but we need to take care of fractions
// so we don't end up in the middle of a day
d.setDate(1);
var start = d.getTime();
d.setMonth(d.getMonth() + 1);
var end = d.getTime();
d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
carry = d.getHours();
d.setHours(0);
}
else
d.setMonth(d.getMonth() + tickSize);
}
else if (unit == "year") {
d.setFullYear(d.getFullYear() + tickSize);
}
else
d.setTime(v + step);
} while (v < axis.max && v != prev);
return ticks;
};
axis.tickFormatter = function (v, axis) {
var d = dateGenerator(v, axis.options);
// first check global format
if (opts.timeformat != null)
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
var span = axis.max - axis.min;
var suffix = (opts.twelveHourClock) ? " %p" : "";
var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
if (t < timeUnitSize.minute)
fmt = hourCode + ":%M:%S" + suffix;
else if (t < timeUnitSize.day) {
if (span < 2 * timeUnitSize.day)
fmt = hourCode + ":%M" + suffix;
else
fmt = "%b %d " + hourCode + ":%M" + suffix;
}
else if (t < timeUnitSize.month)
fmt = "%b %d";
else if (t < timeUnitSize.year) {
if (span < timeUnitSize.year)
fmt = "%b";
else
fmt = "%b %Y";
}
else
fmt = "%Y";
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
return rt;
};
}
});
});
}
$.plot.plugins.push({
init: init,
options: options,
name: 'time',
version: '1.0'
});
})(jQuery);

View File

@@ -0,0 +1,177 @@
r.timeseries = {
_tickSizeByInterval: {
'hour': [1, 'day'],
'day': [7, 'day'],
'month': [1, 'month']
},
_units : ['', 'k', 'M', 'B'],
_tooltip: null,
_currentHover: null,
init: function () {
$('table.timeseries').each($.proxy(function (i, table) {
var series = this.makeTimeSeriesChartsFromTable(table)
this.addBarsToTable(table, series)
}, this))
},
_formatTick: function (val, axis) {
if (val == 0)
return '0'
else if (val < 10)
return val.toFixed(2)
for (var i = 1; i < this._units.length; i++) {
if (val / Math.pow(1000, i - 1) < 1000) {
break
}
}
val /= Math.pow(1000, i - 1)
return val.toFixed(axis.tickDecimals) + this._units[i - 1]
},
makeTimeSeriesChartsFromTable: function (table) {
var table = $(table),
series = this._readFromTable(table),
options = this._configureFlot(table),
chartRow = $('<div>')
$.each(series, function (i, chart) {
var figure = $('<div class="timeseries">')
.append($('<span class="title">').text(chart.caption))
.append('<div class="timeseries-placeholder">')
.appendTo(chartRow)
chart.placeholder = figure.find('.timeseries-placeholder')
})
$('#charts').append(chartRow)
$.each(series, function(i, chart) {
$.plot(chart.placeholder, [chart], options)
})
table.addClass('charted')
return series
},
addBarsToTable: function (table, series) {
var table = $(table),
newColHeader = $('<th scope="col">')
table.find('thead tr').append(newColHeader)
table.find('tbody tr').each(function (i, row) {
var row = $(row),
data = row.children('td'),
newcol = $('<td>')
$.each(series, function (i, s) {
var datum = data.eq(s.index).data('value'),
bar = $('<div>').addClass('timeseries-tablebar')
.css('background-color', s.color)
.width((datum / s.maxValue) * 100)
if (datum > 0)
newcol.append(bar)
if (i === 0 && datum !== 0 && datum === s.maxValue)
row.addClass('max')
})
row.append(newcol)
})
},
_configureFlot: function (table) {
var interval = table.data('interval'),
tickUnit = this._tickSizeByInterval[interval],
unprocessed = $('#timeseries-unprocessed').data('last-processed'),
markings = []
if (unprocessed) {
markings.push({
color: '#eee',
xaxis: {
from: unprocessed
}
})
markings.push({
color: '#aaa',
xaxis: {
from: unprocessed,
to: unprocessed
}
})
}
return {
grid: {
'markings': markings
},
xaxis: {
mode: 'time',
tickSize: tickUnit
},
yaxis: {
min: 0,
tickFormatter: $.proxy(this, '_formatTick')
}
}
},
_readFromTable: function (table) {
var maxPoints = parseInt(table.data('maxPoints'), 10),
headers = table.find('thead tr:last-child th:not(:first-child)'),
series = []
// initialize the series
headers.each(function (i, header) {
var header = $(header),
caption = header.attr('title'),
color = header.data('color')
if (!color) {
return
}
series.push({
'lines': {
'show': true,
'steps': true,
'fill': true
},
'color': color,
'caption': caption,
'data': [],
'maxValue': 0,
'index': i
})
})
// read the data from the table
var rows = table.find('tbody tr')
rows.each(function (i, row) {
var row = $(row),
timestamp = row.children('th').data('value'),
data = row.children('td')
$.each(series, function (j, s) {
var datum = data.eq(s.index).data('value')
// if we have a maximum number of data points to chart, choose
// the most recent ones from the table
if (!maxPoints || i > rows.length - maxPoints)
s.data.push([timestamp, datum])
s.maxValue = Math.max(s.maxValue, datum)
})
})
return series
}
}

View File

@@ -0,0 +1,33 @@
r.traffic = {
init: function () {
// add a simple method of jumping to any subreddit's traffic page
if ($('body').hasClass('traffic-sitewide'))
this.addSubredditSelector()
},
addSubredditSelector: function () {
$('<form>').append(
$('<fieldset>').append(
$('<legend>').text(r.strings.view_subreddit_traffic),
$('<input type="text" id="srname">'),
$('<input type="submit">').attr('value', r.strings.go)
)
).submit(r.traffic._onSubredditSelected)
.prependTo('.traffic-tables-side')
},
_onSubredditSelected: function () {
var srname = $(this.srname).val()
window.location = window.location.protocol + '//' +
r.config.cur_domain +
'/r/' + srname +
'/about/traffic'
return false
}
}
$(function () {
r.traffic.init()
})

View File

@@ -0,0 +1,54 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="reddittraffic.html"/>
<%!
from r2.lib.template_helpers import format_number
%>
<%def name="sidetables()">
<table class="traffic-table">
<caption>${_("top adverts")}</caption>
<thead>
<tr>
<th scope="col">${_("ad")}</th>
<th scope="col">${_("uniques")}</th>
<th scope="col">${_("impressions")}</th>
</tr>
</thead>
<tbody>
% for (name, url), data in thing.advert_summary:
<tr>
<th scope="row"><a href="${url}" title="${name}">${name[:25]}</a></th>
% for datum in data:
<td>${format_number(datum)}</td>
% endfor
</tr>
% endfor
</tbody>
</table>
</%def>
<%def name="tables()">
${thing.totals}
</%def>

View File

@@ -0,0 +1,48 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.template_helpers import format_number
%>
<h1>${_("language traffic statistics")}</h1>
<table class="traffic-table">
<caption>${_("traffic by language")}</caption>
<thead>
<tr>
<th scope="col">${_("language")}</th>
<th scope="col">${_("uniques")}</th>
<th scope="col">${_("pageviews")}</th>
</tr>
</thead>
<tbody>
% for (langcode, langname), data in thing.language_summary:
<tr>
<th scope="row"><a href="/traffic/languages/${langcode}">${langname}</a></th>
% for datum in data:
<td>${format_number(datum)}</td>
% endfor
</tr>
% endfor
</tbody>
</table>

View File

@@ -28,14 +28,17 @@
import babel.numbers
locale = c.locale
def num(x):
return format_number(x, locale)
def money(x):
return babel.numbers.format_currency(x, 'USD', locale)
%>
<h1>Sponsored link calendar</h1>
${unsafe(js.use('flot'))}
<%namespace file="reddittraffic.html" import="load_timeseries_js"/>
${load_timeseries_js()}
<h1>${_("Sponsored link calendar")}</h1>
%if not c.user_is_sponsor:
<div class="instructions">
@@ -171,114 +174,12 @@ ${unsafe(js.use('flot'))}
</div>
<div class="clear"></div>
%if c.user_is_sponsor:
<h1>${_("total promotion traffic")}</h1>
<div>
%if thing.imp_graph:
${thing.imp_graph}
%endif
%if thing.cli_graph:
${thing.cli_graph}
%endif
%if thing.money_graph:
${thing.money_graph}
%endif
</div>
%endif
<h1>${_("historical site performance")}</h1>
<div>
%if thing.cpm_graph:
${thing.cpm_graph}
%endif
%if thing.cpc_graph:
${thing.cpc_graph}
%endif
%if thing.cpc_graph and c.user_is_sponsor:
${thing.ctr_graph}
%endif
<div id="charts"></div>
${thing.performance_table}
<script type="text/javascript">
r.timeseries.init()
</script>
</div>
%if c.user_is_sponsor:
<div class="clear"></div>
%if thing.top_promoters:
<h1>${_('top promoters this month')}</h1>
<table class="traffic-table">
<tr>
<th>user</th>
<th>promos</th>
<th>bids</th>
<th>credits</th>
<th>total</th>
<th>per promo</th>
</tr>
%for account, bid, refund, promos in thing.top_promoters:
<tr>
<td>${account.name}</td>
<td>${len(promos)}</td>
<td>${money(bid)}</td>
<td>${money(refund)}</td>
<td>${money(bid - refund)}</td>
<td>${money((bid - refund)/len(promos))}</td>
</tr>
%endfor
%if len(thing.top_promoters) != 1:
<%
totals = zip(*thing.top_promoters)
total_bid = sum(totals[1])
total_refund = sum(totals[2])
total_promos = sum(map(len, totals[3]))
%>
<tr class="totals">
<td>Total</td>
<td>${total_promos}</td>
<td>${money(total_bid)}</td>
<td>${money(total_refund)}</td>
<td>${money(total_bid - total_refund)}</td>
<td>${money((total_bid - total_refund)/total_promos)}</td>
</tr>
%endif
</table>
%endif
%if thing.recent:
<h1>${_('promotions this month')}&nbsp;<a href="/promoted/graph.csv">[CSV]</a></h1>
<table class="traffic-table">
<tr>
<th colspan="2">Impresions</th>
<th colspan="2">Clicks</th>
<th></th>
<th></th>
</tr>
<tr>
<th>date</th>
<th>unique</th>
<th>total</th>
<th>unique</th>
<th>total</th>
<th>price</th>
<th>points</th>
<th>title</th>
</tr>
%for link, uimp, nimp, ucli, ncli in thing.recent:
<tr>
<td style="white-space:nowrap">
${link._date.strftime("%Y-%m-%d")}
</td>
<td>${num(uimp)}</td>
<td>${num(nimp)}</td>
<td>${num(ucli)}</td>
<td>${num(ncli)}</td>
<td>TODO</td>
## <td>${money(link.promote_bid)}</td>
<td>${link._ups - link._downs}</td>
<td style="text-align:left">
<a class="title" href="${link.permalink}">${link.title}</a>
</td>
</tr>
%endfor
</table>
%endif
%endif

View File

@@ -0,0 +1,103 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="reddittraffic.html"/>
<%namespace file="reddittraffic.html" import="load_timeseries_js"/>
<%!
from r2.lib.strings import strings
from r2.lib.filters import safemarkdown
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}">
<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>
</tr>
</thead>
<tbody>
% for date, data in thing.history:
<tr>
<th scope="col" data-value="${js_timestamp(date)}">${date}</th>
% for datum in data:
<td data-value="${datum}">${format_number(datum)}</td>
% endfor
</tr>
% endfor
</tbody>
<tfoot>
<tr>
<th scope="row">
${_("total")}
% if thing.is_preliminary:
*
% 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:
<td>--</td>
% endif
</tr>
</tfoot>
</table>
% if thing.is_preliminary:
<p class="totals-are-preliminary">* ${_("totals are preliminary until 24 hours after the end of the promotion.")}</p>
% endif
</%def>

View File

@@ -1,117 +0,0 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from r2.lib.template_helpers import static, format_number
from r2.lib import js
from r2.models.subreddit import DomainSR, FakeSubreddit
locale = c.locale
def num(x):
return format_number(x, locale)
%>
${unsafe(js.use('flot'))}
<div class="promoted-traffic">
<div class="instructions">
<p>
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 till 24 hours after the link has finished its run.
</p>
<p>
Also below is a form which can be used to share traffic results with another user.
</p>
</div>
${thing.viewers}
%if thing.traffic:
<h1>
${_("Promotion Traffic")}
<a href="/traffic/${thing.thing._id36}.csv">
${_("(download as .csv)")}
</a>
</h1>
%if thing.imp_graph:
${thing.imp_graph}
%endif
%if thing.cli_graph:
${thing.cli_graph}
%endif
<table class="traffic-table">
<tr>
<th>date</th>
<th colspan="2">Impresions</th>
<th colspan="2">Clicks</th>
<th colspan="2">click-thru (%)</th>
</tr>
<tr>
<th></th>
<th>unique</th>
<th>total</th>
<th>unique</th>
<th>total</th>
<th>unique</th>
<th>total</th>
</tr>
%for x in thing.to_iter():
<tr>
%if x[1] != '0' or x[2] != '0':
%for y in x:
<td>${y}</td>
%endfor
%endif
</tr>
%endfor
%if thing.totals:
<%
uimp, nimp, ucli, ncli = thing.totals
%>
<tr style="font-weight:bold">
<td>
total
%if thing.preliminary:
(users approx)
%endif
</td>
<td ${"class='prelim'" if thing.preliminary else ''}>
${num(uimp)}${'*' if thing.preliminary else ''}
</td>
<td>${num(nimp)}</td>
<td ${"class='prelim'" if thing.preliminary else ''}>
${num(ucli)}${'*' if thing.preliminary else ''}
</td>
<td>${num(ncli)}</td>
<td>${("%.2f%%" % (float(100*ucli) / uimp)) if uimp else "--.--%"}</td>
<td>${("%.2f%%" % (float(100*ncli) / nimp)) if nimp else "--.--%"}</td>
</tr>
%if thing.preliminary:
<tr>
<td colspan="7" class="prelim error">
* totals are preliminary until 24 hours after the end of the promotion.
</td>
</tr>
%endif
%endif
</table>
%endif
</div>

View File

@@ -21,241 +21,93 @@
###############################################################################
<%!
from r2.lib.template_helpers import static, format_number
from r2.lib import js
from r2.models.subreddit import DomainSR, FakeSubreddit
locale = c.locale
def num(x):
return format_number(x, locale)
%>
import babel.dates
${unsafe(js.use('flot'))}
from r2.lib import js
from r2.lib.strings import strings
from r2.lib.template_helpers import static, js_timestamp, format_number
<%def name="daily_summary()">
<%
thing.day_data = filter(None, thing.day_data)
umin = min(data[0] for date, data in thing.day_data)
if len(filter(lambda x: x[1][0] == umin, thing.day_data)) > 1:
umin = -1
umax = max(data[0] for date, data in filter(None, thing.day_data))
%>
<table class="traffic-table">
<tr>
%if c.site.domain:
<th></th>
<th colspan="2">total</th>
<th colspan="2">${c.site.domain}</th>
</tr><tr>
<th>${_("date")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
<th>${_("subscriptions")}</th>
%else:
<th>${_("date")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
%if not c.default_sr:
<th>${_("subscriptions")}</th>
%endif
</tr>
%endif
</tr>
%for x, (date, data) in enumerate(thing.day_data):
<tr class="${'odd' if x % 2 else 'even'} ${'max' if data[0] == umax else 'min' if data[0] == umin else ''}">
<td>${date.strftime("%Y-%m-%d")}</td>
<%
indx = range(5) if c.site.domain else \
[0,1,4] if not c.default_sr else [0,1]
%>
%for i in indx:
<td>${num(data[i]) if data[i] else "-"}</td>
%endfor
${bars(data[0], data[1])}
</tr>
%endfor
</table>
import babel.dates
%>
<%def name="load_timeseries_js()">
<!--[if lte IE 8]>
${unsafe(js.use('timeseries-ie'))}
<![endif]-->
<!--[if !(lte IE 8)]><!-->
${unsafe(js.use('timeseries'))}
<!--<![endif]-->
</%def>
<%def name="weekly_summary()">
<table class="traffic-table">
<tr>
<th colspan="3">Weekly summary</th>
</tr>
<tr>
<th>${_("date")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
</tr>
<%
dow = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday", "Sunday"]
%>
%for i, (uni, imp) in enumerate(zip(thing.uniques_by_dow, thing.impressions_by_dow)):
<tr class="${'odd' if i % 2 else 'even'}">
<td>${dow[i]}</td>
<td>${num(int(uni))}</td>
<td>${num(int(imp))}</td>
${bars(uni, imp)}
</tr>
%endfor
<tr class="mean">
<td>${_("Daily Mean")}</td>
<td>${num(int(thing.uniques_mean))}</td>
<td>${num(int(thing.impressions_mean))}</td>
</tr>
</table>
</%def>
% if thing.place:
<h1>${_("traffic statistics for %(place)s") % dict(place=thing.place)}</h1>
% endif
<h1>Traffic for ${c.site.name}</h1>
${self.preamble()}
<p>
Below are the traffic stats for your reddit. Each graph represents one of the following over the interval specified
</p>
<ul style="margin:7px">
<li>
<b>pageviews</b> &#32;are all hits to ${c.site.name}, including both listing pages and comment pages.
</li>
<li>
<b>uniques</b> &#32;are the total number of unique visitors (IP and U/A combo) that generate the above pageviews. This is independent of whether or not they are logged in.
</li>
<li>
<b>subscriptions</b> &#32;is the number of new subscriptions in a given day that have been generated. This number is less accurate than the first two metrics, as, though we can track new subscriptions, we have no way to track unsubscriptions (which are when a subscription is actually deleted from the db).
</li>
</ul>
<p>
Note: there are a couple of places outside of your reddit where someone can click "subscribe", so it is possible (though unlikely) that the subscription count can exceed the unique count on a given day.
<p id="timeseries-unprocessed" data-last-processed="${js_timestamp(thing.traffic_last_modified)}">
% if thing.traffic_lag.total_seconds() > 10800:
${strings.traffic_processing_slow % dict(date=babel.dates.format_datetime(thing.traffic_last_modified, format="long", locale=c.locale))}
% else:
${strings.traffic_processing_normal % dict(date=babel.dates.format_datetime(thing.traffic_last_modified, format="long", locale=c.locale))}
% endif
</p>
%if not thing.has_data:
<p class="error">
${_("There doesn't seem to be any traffic data at the moment. Please check back later.")}
</p>
%else:
<table>
<tr>
<td>
${thing.uniques_hour}
</td>
<td>
${thing.impressions_hour}
</td>
</tr>
<tr>
<td>
${thing.uniques_day}
</td>
<td>
${thing.impressions_day}
</td>
</tr>
<tr>
<td>
${thing.uniques_month}
</td>
<td>
${thing.impressions_month}
</td>
</tr>
%if not c.default_sr:
<tr>
<td>
${thing.subscriptions_day}
</td>
<td>
</td>
</tr>
%endif
</table>
<div id="charts"></div>
<div style="float:left">
${weekly_summary()}
%if c.default_sr:
${daily_summary()}
%endif
</div>
<div class="traffic-tables-side">
${self.sidetables()}
</div>
%if c.default_sr:
<% data = thing.monthly_summary() %>
<table class="traffic-table">
<tr>
<th colspan="3">Monthly data</th>
</tr>
<tr>
<th>${_("date")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
</tr>
%for i, d in enumerate(reversed(data)):
<tr class="${'odd' if i % 2 else 'even'}">
%for cls, x in d:
<td class="${cls}">${x}</td>
%endfor
</tr>
%endfor
</table>
<table class="traffic-table">
<tr>
<th></th>
<th colspan="2">total</th>
<th colspan="2">${_("cnamed")}</th>
</tr>
<tr>
<th>${_("date")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
<th>${_("uniques")}</th>
<th>${_("impressions")}</th>
<th>${_("cname")}</th>
</tr>
%for i, (sr, d) in enumerate(thing.reddits_summary()):
<tr class="${'odd' if i % 2 else 'even'}">
<td
%if isinstance(sr, FakeSubreddit):
style="font-style: left; text-align: left"
%else:
style="font-weight:bold; text-align: left"
%endif
>
%if isinstance(sr, DomainSR):
<a href="${sr.path}">[domain:${sr.name}]</a>
%elif isinstance(sr, FakeSubreddit):
<a href="${sr.path}">[meta:${sr.name}]</a>
%else:
<a href="${sr.path}about/traffic">${sr.name}</a>
%endif
</a>
</td>
%for x in d:
<td>${num(x) if x else "--"}</td>
%endfor
<td>
%if not isinstance(sr, FakeSubreddit) and sr.domain:
<a href="http://${sr.domain}/">${sr.domain}</a>
%endif
</td>
</tr>
%endfor
</table>
%else:
${daily_summary()}
%endif
%endif
<div class="traffic-tables">
${self.tables()}
</div>
<script type="text/javascript">
r.timeseries.init()
</script>
<%def name="preamble()" />
<%def name="bars(uni, imp)">
<%def name="sidetables()">
<%
wid_uni = int(50 * min(2, float(uni)/max(thing.uniques_mean, 1)))
wid_imp = int(50 * min(2, float(imp)/max(thing.impressions_mean,1)))
%>
<td>
<div style="height:4px; width:${wid_imp}px; background-color:#336699; margin-bottom:1px">
</div>
<div style="height:4px; width:${wid_uni}px; background-color:#FF4500">
</div>
</td>
day_names = babel.dates.get_day_names(locale=c.locale)
%>
% if thing.dow_summary:
<table class="traffic-table">
<caption>${_("traffic by day of week")}</caption>
<thead>
<tr>
<th scope="col">${_("day")}</th>
<th scope="col">${_("uniques")}</th>
<th scope="col">${_("pageviews")}</th>
</tr>
</thead>
<tbody>
% for date, cols in thing.dow_summary:
<tr>
<th scope="row">${day_names[date.weekday()]}</th>
% for col in cols:
<td>${format_number(col)}</td>
% endfor
</tr>
% endfor
</tbody>
<tfoot>
<tr>
<th scope="row">${_("daily mean")}</th>
% for col in thing.dow_means:
<td>${format_number(col)}</td>
% endfor
</tr>
</tfoot>
</table>
% endif
</%def>
<%def name="tables()">
% for table in thing.tables:
${table}
% endfor
</%def>

View File

@@ -0,0 +1,56 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="reddittraffic.html"/>
<%!
from r2.lib.template_helpers import format_number
%>
<%def name="sidetables()">
${parent.sidetables()}
<table class="traffic-table">
<caption>${_("top subreddits")}</caption>
<thead>
<tr>
<th scope="col">${_("subreddit")}</th>
<th scope="col">${_("uniques")}</th>
<th scope="col">${_("pageviews")}</th>
</tr>
</thead>
<tbody>
% for (name, url), data in thing.subreddit_summary:
<tr>
% if url:
<th scope="row"><a href="${url}">${name}</a></th>
% else:
<th scope="row">${name}</th>
% endif
% for datum in data:
<td>${format_number(datum)}</td>
% endfor
</tr>
% endfor
</tbody>
</table>
</%def>

View File

@@ -0,0 +1,32 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="trafficpage.html"/>
<%!
from r2.lib import js
%>
<%def name="javascript()">
${parent.javascript()}
${unsafe(js.use('traffic'))}
</%def>

View File

@@ -0,0 +1,44 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="reddittraffic.html"/>
<%namespace file="reddittraffic.html" import="load_timeseries_js"/>
<%!
from r2.lib.filters import safemarkdown
from r2.lib.strings import strings
%>
<%def name="preamble()">
% if c.user_is_sponsor:
<div class="md">
<ul>
<li><a href="/traffic/adverts/${c.site._fullname}">${_("300x100 traffic for this subreddit.")}</a></li>
<li><a href="/traffic/adverts/dart_${c.site.name}">${_("DART traffic for this subreddit.")}</a></li>
</ul>
</div>
% else:
${unsafe(safemarkdown(strings.traffic_subreddit_explanation % dict(subreddit=thing.place)))}
% endif
${load_timeseries_js()}
</%def>

View File

@@ -0,0 +1,69 @@
## 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 CondeNet, Inc.
##
## All portions of the code written by CondeNet are Copyright (c) 2006-2010
## CondeNet, Inc. All Rights Reserved.
################################################################################
<%!
import time
import babel.dates
from r2.lib.template_helpers import js_timestamp, format_number
%>
<%
month_names = babel.dates.get_month_names(locale=c.locale)
%>
<table id="${thing.id}" class="timeseries ${thing.classes}" data-interval="${thing.interval}">
<caption>${thing.title}</caption>
<thead>
<tr>
<th scope="col">${_("date")}</th>
% for col in thing.columns:
% if "color" in col:
<th scope="col" title="${col['title']}" data-color="${col['color']}">${col['shortname']}</th>
% else:
<th>${col['shortname']}</th>
% endif
% endfor
</tr>
</thead>
<tbody>
% for date, data in thing.rows:
<tr>
<th data-value="${js_timestamp(date)}" scope="row">
% if thing.interval == "hour":
${babel.dates.format_datetime(date, format="short", locale=c.locale)}
% elif thing.interval == "day":
${babel.dates.format_date(date, format="short", locale=c.locale)}
% else:
${month_names[date.month]}
% endif
</th>
% for datum in data:
% if date < thing.latest_available_data:
<td data-value="${datum}">${format_number(datum)}</td>
% else:
<td data-value="-1">${_("unavailable")}</td>
% endif
% endfor
</tr>
% endfor
</tbody>
</table>

View File

@@ -1,75 +0,0 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%!
from random import random
import simplejson
%>
<%
_id = str(random()).split('.')[-1]
%>
<div class="traffic-graph">
<div class="title" style="width:${thing.width}px;">${thing.title}</div>
<div id="placeholder-${_id}"
style="width:${thing.width}px;height:${thing.height}px">
</div>
</div>
<script id="source" language="javascript" type="text/javascript">
$(function () {
if($.browser.msie) {
var i = new Image();
i.src = "${unsafe(thing.gc)}";
$("#placeholder-${_id}").parent().html($(i));
} else {
$.plot($("#placeholder-${_id}"), [
%for i, s in enumerate(thing.data):
<%
color = thing.colors[i] if i < len(thing.colors) else thing.colors[-1]
%>
{
data: ${simplejson.dumps(s)},
color: "#${color}",
lines: { show: true, fill: ${"true" if len(thing.data) == 1 else "false"} }
}${',' if i != len(thing.data)-1 else ''}
%endfor
],
{
xaxis: { mode: "time"},
yaxis: {min: 0,
tickFormatter: function(val, axis) {
if (val >= 2000000) {
return (val / 1000000).toFixed(axis.tickDecimals) + "M";
} else if (val >= 2000) {
return (val / 1000).toFixed(axis.tickDecimals) + "k";
} else if (val < 10) {
return val.toFixed(2);
}
return val.toFixed(axis.tickDecimals);
}
},
selection: { mode: "x" }
});
}});
</script>

View File

@@ -0,0 +1,29 @@
## 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-2012
## reddit Inc. All Rights Reserved.
###############################################################################
<%inherit file="reddit.html"/>
<%namespace file="reddittraffic.html" import="load_timeseries_js"/>
<%def name="javascript()">
${parent.javascript()}
${load_timeseries_js()}
</%def>