mirror of
https://github.com/reddit-archive/reddit.git
synced 2026-01-24 14:27:58 -05:00
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:
10
r2/Makefile
10
r2/Makefile
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -173,6 +173,10 @@ menu = MenuHandler(hot = _('hot'),
|
||||
pending_promos = _('pending'),
|
||||
rejected_promos = _('rejected'),
|
||||
|
||||
sitewide = _('sitewide'),
|
||||
languages = _('languages'),
|
||||
adverts = _('adverts'),
|
||||
|
||||
whitelist = _("whitelist")
|
||||
)
|
||||
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
430
r2/r2/lib/pages/trafficpages.py
Normal file
430
r2/r2/lib/pages/trafficpages.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1
r2/r2/public/static/js/lib/excanvas.min.js
vendored
Normal file
1
r2/r2/public/static/js/lib/excanvas.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
308
r2/r2/public/static/js/lib/jquery.flot.time.js
Normal file
308
r2/r2/public/static/js/lib/jquery.flot.time.js
Normal 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);
|
||||
177
r2/r2/public/static/js/timeseries.js
Normal file
177
r2/r2/public/static/js/timeseries.js
Normal 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
|
||||
}
|
||||
}
|
||||
33
r2/r2/public/static/js/traffic.js
Normal file
33
r2/r2/public/static/js/traffic.js
Normal 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()
|
||||
})
|
||||
54
r2/r2/templates/adverttrafficsummary.html
Normal file
54
r2/r2/templates/adverttrafficsummary.html
Normal 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>
|
||||
48
r2/r2/templates/languagetrafficsummary.html
Normal file
48
r2/r2/templates/languagetrafficsummary.html
Normal 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>
|
||||
@@ -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')} <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
|
||||
|
||||
103
r2/r2/templates/promotedlinktraffic.html
Normal file
103
r2/r2/templates/promotedlinktraffic.html
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>  are all hits to ${c.site.name}, including both listing pages and comment pages.
|
||||
</li>
|
||||
<li>
|
||||
<b>uniques</b>  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>  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>
|
||||
|
||||
56
r2/r2/templates/sitewidetraffic.html
Normal file
56
r2/r2/templates/sitewidetraffic.html
Normal 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>
|
||||
32
r2/r2/templates/sitewidetrafficpage.html
Normal file
32
r2/r2/templates/sitewidetrafficpage.html
Normal 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>
|
||||
44
r2/r2/templates/subreddittraffic.html
Normal file
44
r2/r2/templates/subreddittraffic.html
Normal 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>
|
||||
69
r2/r2/templates/timeserieschart.html
Normal file
69
r2/r2/templates/timeserieschart.html
Normal 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>
|
||||
@@ -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>
|
||||
29
r2/r2/templates/trafficpage.html
Normal file
29
r2/r2/templates/trafficpage.html
Normal 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>
|
||||
Reference in New Issue
Block a user