app stats implementation (bug 695236)

This commit is contained in:
Davor Spasovski 2012-03-28 01:25:01 -07:00
Родитель e03b1cf9f2
Коммит b0ccd4e11f
27 изменённых файлов: 1210 добавлений и 50 удалений

Просмотреть файл

@ -1,4 +1,5 @@
import jinja2
import waffle
from jingo import env, register
from tower import ugettext as _
@ -20,6 +21,8 @@ def report_menu(context, request, report, obj=None):
obj.has_author(request.amo_user))):
has_privs = True
t = env.get_template('stats/addon_report_menu.html')
if obj.is_webapp() and waffle.switch_is_active('marketplace'):
t = env.get_template('appstats/app_report_menu.html')
c = {
'addon': obj,
'has_privs': has_privs

Просмотреть файл

@ -0,0 +1,36 @@
<div class="modal" id="stats-note">
<a class="close">{{ _('close') }}</a>
{% block stats_note %}{% endblock %}
</div>
<div class="modal" id="exception-note">
<a class="close">{{ _('close') }}</a>
<h2></h2>
<div></div>
</div>
<div class="modal" id="custom-criteria">
<h2>{{ _('Custom Date Range') }}</h2>
<a class="close">{{ _('close') }}</a>
<form id="date-range-form">
<fieldset>
<p>
<label for="date-range-start">{{ _('From') }}</label>
<input type="date" id="date-range-start">
</p>
<div id="start-date-picker"></div>
</fieldset>
<fieldset>
<p>
<label for="date-range-end">{{ _('To') }}</label>
<input type="date" id="date-range-end">
</p>
<div id="end-date-picker"></div>
</fieldset>
<footer>
<p>
<button id="date-range-submit" type="submit">
{{ _('Update') }}
</button>
</p>
</footer>
</form>
</div>

Просмотреть файл

@ -193,46 +193,12 @@
{% endblock %}
{% block popups %}
<div class="modal" id="stats-note">
<a class="close">{{ _('close') }}</a>
{% block stats_note %}
{% endblock %}
</div>
<div class="modal" id="exception-note">
<a class="close">{{ _('close') }}</a>
<h2></h2>
<div></div>
</div>
<div class="modal" id="custom-criteria">
<h2>{{ _('Custom Date Range') }}</h2>
<a class="close">{{ _('close') }}</a>
<form id="date-range-form">
<fieldset>
<p>
<label for="date-range-start">{{ _('From') }}</label>
<input type="date" id="date-range-start">
</p>
<div id="start-date-picker"></div>
</fieldset>
<fieldset>
<p>
<label for="date-range-end">{{ _('To') }}</label>
<input type="date" id="date-range-end">
</p>
<div id="end-date-picker"></div>
</fieldset>
<footer>
<p>
<button id="date-range-submit" type="submit">{{ _('Update') }}</button>
</p>
</footer>
</form>
</div>
{% include 'popup.html' %}
{% endblock %}
{% block js %}
<!--[if IE]>
<script src="{{ media('js/lib/excanvas.compiled.js" type="text/javascript') }}"></script>
<script src="{{ media('js/lib/excanvas.compiled.js') }}"></script>
<![endif]-->
{{ js('zamboni/stats') }}
{% endblock %}

Просмотреть файл

@ -31,7 +31,10 @@
font-size: 12px;
line-height: 14px;
margin-left: 7px;
padding: 7px 14px 9px;
padding: 8px 14px;
&:hover {
text-decoration: none;
}
}
}
}

Просмотреть файл

@ -217,3 +217,7 @@ body {
padding: 16px;
text-align: center;
}
.hidden {
display: none;
}

451
media/css/mkt/stats.less Normal file
Просмотреть файл

@ -0,0 +1,451 @@
@import 'lib';
.statistics {
.island {
float: none;
margin-bottom: 2em;
}
hgroup {
clear: both;
}
.ui-helper-hidden-accessible {
display: none !important;
}
}
.html-rtl.statistics #page .header-search {
right: auto;
left: 20px;
}
.secondary {
.island {
float: none;
}
> aside, > div {
margin-bottom: 2em;
}
}
/**
* Rules for date criteria selection
**/
.island.criteria {
padding: 0 0 0 12px;
margin: 0 0 0 1em;
z-index: 1000;
float: right;
ul {
line-height: 2.5em;
}
li {
color: @dark-gray;
font-weight: bold;
display: inline;
a {
font-weight: normal;
border: 1px solid transparent;
color: @link;
padding: 4px 8px;
text-decoration: none;
white-space: nowrap;
&:hover {
text-decoration: underline;
}
}
&.selected a {
font-weight: bold;
color: @orange;
}
a.inactive {
color: @light-gray;
cursor: default;
&:hover {
text-decoration: none;
}
}
}
}
.html-rtl .island.criteria {
float: left;
margin: 0 1em 0 0;
padding: 0 12px 0 0;
}
/**
* Three-up stats
**/
.two-up {
position: relative;
overflow: hidden;
div {
.border-box();
float: left;
text-align: left;
width: 50%;
position: relative;
padding-left: 2em;
color: @dark-gray;
&:first-child {
padding-left: 1em;
}
&:first-child:after {
content: '';
display: block;
position: absolute;
top: 0;
right: 0;
border-left: 1px dotted @medium-gray;
height: 100%;
}
b {
font-size: 150%;
}
a {
line-height: 2rem;
display: block;
font-weight: bold;
font-size: 140%;
}
small {
line-height: 2rem;
font-size: 110%;
}
}
}
/**
* bar-chart tables
**/
.csv-table {
.table-box {
width: 100%;
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
thead th {
white-space: normal;
overflow: hidden;
text-overflow: ellipsis;
}
th, td {
width: 100px;
padding: .5em 1em;
text-align: right;
&:first-child {
text-align: left;
width: 200px;
}
}
th {
font-weight: bold;
}
tbody {
display: none;
tr {
&:nth-child(2n+1) {
background: fadeOut(@medium-gray, 80%);
}
}
}
.paginator {
float: none;
.rel {
margin-top: 20px;
width: 100%;
}
}
}
/**
* sidestats
**/
aside.highlight {
display: block;
}
#stats-note {
dt {
margin-top: 1em;
}
dd {
font-size: .8em;
}
code {
font-family: @open-stack;
background: @light-gray;
color: @dark-gray;
font-size: .9em;
padding: 2px 4px;
}
}
#export_data {
font: 13px "helvetica neue",arial,helvetica,sans-serif;
margin-left: 1em;
}
/**
* Big table
**/
table.stats-aggregate {
width: 100%;
margin-top: .5em;
}
table.stats-aggregate thead th {
text-align: right;
padding-right: 72px;
line-height: 90%;
}
table.stats-aggregate thead th:first-child {
text-align: left;
padding-right: inherit;
}
table.stats-aggregate tbody td {
line-height: 120%;
font-size: 140%;
}
table.stats-aggregate tbody td.value {
padding: 5px 0 3 0px;
text-align: right;
}
table.stats-aggregate tbody td.label {
padding-left: 2px;
}
table.stats-aggregate tbody span.change {
display: block;
float: right;
width: 54px;
padding-left: .5em;
text-align: left;
font-size: 80%;
padding-top: 2px;
}
table.stats-aggregate tbody span.change.plus {
color: #00774d;
}
table.stats-aggregate tbody span.change.minus {
color: #850000;
}
#stats {
.loading, .loaded {
position:relative;
}
.loading:after {
-moz-transition: opacity .5s;
content: "\00a0";
display: block;
position: absolute;
background: #040204 url("../../img/zamboni/loading.gif") no-repeat center center;
opacity: .4;
width: 100%;
height: 100%;
z-index: 1000;
top: 0;
left: 0;
}
.loaded:after {
opacity: 0;
pointer-events: none;
}
footer {
padding-top: 0;
border-top: 1px solid @medium-gray;
}
}
.chart {
position: relative;
padding: 0;
width: 100%;
overflow: hidden;
.border-radius(8px);
border: 2px solid @light-gray;
.border-box();
}
#head-chart {
position: relative;
background: #fff;
overflow: hidden;
height: 384px;
}
.no-data-overlay {
display: none;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
color: #fff;
p {
text-align: center;
position: relative;
top: 35%;
color: @light-gray;
font-size: 2em;
}
}
.nodata .no-data-overlay {
display: block;
}
.loadmessage {
position: fixed;
left: 0;
right: 0;
top: -2.5em;
height: 2em;
margin: 0 auto;
text-align: center;
pointer-events: none;
-moz-transition: top .5s;
-webkit-transition: top .5s;
transition: top .5s;
z-index:9000;
}
.loadmessage span {
background: url("../../img/zamboni/loading-small.gif") no-repeat 1em center;
background-color: #000;
padding: .5em 1em;
padding-left: 36px;
line-height: 2em;
color: #fff;
opacity: .75;
-moz-border-radius: 0 0 .75em .75em;
-webkit-border-radius: 0 0 .75em .75em;
border-radius: 0 0 .75em .75em;
}
.loadmessage.on {
top: 0;
}
.loadmessage.off {
top: -2.5em;
}
/* Field Menu */
#fieldMenu, #fieldList {
margin-bottom: 0;
}
#fieldList label {
display: block;
white-space: nowrap;
padding-right: 2em;
}
#fieldList label:hover {
background: #ccf;
}
#fieldMenu button {
width: 100%;
}
#fieldList {
max-height: 300px;
min-width: 160px;
overflow-y: auto;
}
#custom-criteria {
form {
overflow: hidden;
text-align: center;
}
fieldset {
display: inline-block;
vertical-align: top;
border: 0;
padding: 0;
&:first-child {
margin: 0 8px 0 0;
}
p {
margin: 0 0 1em;
text-align: left;
}
}
footer {
clear: left;
border-top: 0;
margin-top: 0;
padding: 0;
p {
text-align: left;
margin-bottom: 2px;
}
}
h2 {
margin-bottom: 8px;
}
}
.html-rtl #custom-criteria {
fieldset {
&:first-child {
margin: 0 0 0 8px;
}
p {
text-align: right;
}
}
footer p {
text-align: right;
}
}
#stats-permissions p {
margin-top: .1em;
}
#side-nav .active a {
background-color: @link;
color: @dark-gray;
font-weight: bold;
&:after {
color: inherit;
}
}
#popup-container {
display: none;
}
.modal {
background-color: @white;
border: 3px solid @medium-gray;
.border-radius(8px);
padding: 8px;
.close {
background: url("../../img/impala/banner-close.png") no-repeat scroll 0 0 transparent;
border-radius: 4px 4px 4px 4px;
cursor: pointer;
height: 25px;
margin: 0;
overflow: hidden;
position: absolute;
right: 1em;
text-indent: -1000em;
top: 1em;
width: 25px;
&:hover {
background-position: -25px 0;
background-color: #c40000;
}
}
}

Просмотреть файл

@ -79,9 +79,11 @@
"versions" : "users",
"statuses" : "users",
"users_created" : "users",
"installs" : "users",
"downloads" : "downloads",
"sources" : "downloads",
"contributions" : "currency",
"sales" : "currency",
"reviews_created" : "reviews",
"addons_in_use" : "addons",
"addons_created" : "addons",
@ -121,7 +123,8 @@
series = {},
events = obj.events,
chartRange = {},
t, row, i, field, val;
t, row, i, field, val,
is_overview = metric == 'overview' || metric == 'app_overview';
if (!(group in acceptedGroups)) {
group = 'day';
@ -208,7 +211,7 @@
xFormatter = dayFormatter;
}
if (metric == 'overview') {
if (is_overview) {
return function() {
var ret = "<b>" + xFormatter(this.x) + "</b>",
p;
@ -268,7 +271,7 @@
// Set up the new chart's configuration.
var newConfig = $.extend(baseConfig, { series: chartData });
// set up dual-axes for the overview chart.
if (metric == "overview" && newConfig.series.length) {
if (is_overview && newConfig.series.length) {
_.extend(newConfig, {
yAxis : [
{ // Downloads
@ -300,8 +303,14 @@
}
});
// set Daily Users series to use the right yAxis.
_.find(newConfig.series,
if (metric == 'overview') {
_.find(newConfig.series,
function(s) { return s.id == 'updates'; }).yAxis = 1;
} else {
_.find(newConfig.series,
function(s) { return s.id == 'usage'; }).yAxis = 1;
}
}
if (metric == "contributions" && newConfig.series.length) {
_.extend(newConfig, {

15
media/js/impala/stats/controls.js поставляемый
Просмотреть файл

@ -4,10 +4,13 @@
var $rangeSelector = $(".criteria.range ul"),
$customRangeForm = $("div.custom.criteria"),
$groupSelector = $(".criteria.group ul"),
minDate = Date.iso($('.primary').attr('data-min-date'));
minDate = Date.iso($('.primary').attr('data-min-date')),
msDay = 24 * 60 * 60 * 1000; // One day in milliseconds.
$.datepicker.setDefaults({showAnim: ''});
var $customModal = $("#custom-criteria").modal("#custom-date-range", { width: 520, hideme: false });
var $customModal = $("#custom-criteria").modal("#custom-date-range",
{ width: 520,
hideme: false });
var $startPicker = $("#start-date-picker").datepicker({
maxDate: 0,
minDate: minDate,
@ -52,7 +55,7 @@
// Trim nRange.end by one day if custom range.
if (newState.range.custom) {
nRange.end = new Date(nRange.end.getTime() - (24 * 60 * 60 * 1000));
nRange.end = new Date(nRange.end.getTime() - msDay);
endStr = nRange.end.iso();
}
@ -66,10 +69,12 @@
var newRange = newState.range,
$rangeEl = $('li[data-range="' + newRange + '"]');
if ($rangeEl.length) {
$rangeSelector.children("li.selected").removeClass("selected");
$rangeSelector.children("li.selected")
.removeClass("selected");
$rangeEl.addClass("selected");
} else {
$rangeSelector.children("li.selected").removeClass("selected");
$rangeSelector.children("li.selected")
.removeClass("selected");
$('li[data-range="custom"]').addClass("selected");
}
} else {

Просмотреть файл

@ -32,6 +32,12 @@ var csv_keys = {
ratings: {
'count': gettext('Ratings')
},
sales: {
'count': gettext('Sales')
},
installs: {
'count': gettext('Installs')
},
sources: {
"null" : gettext('Unknown'),
'api' : gettext('Add-ons Manager'),
@ -96,6 +102,11 @@ var csv_keys = {
'downloads' : gettext('Downloads'),
'updates' : gettext('Daily Users')
},
app_overview: {
'installs': gettext('Installs'),
'sales': gettext('Sales'),
'usage': gettext('Usage')
},
apps : {
'{ec8030f7-c20a-464f-9b0e-13a3a9e97384}' : gettext('Firefox'),
'{86c18b42-e466-45a9-ae7a-9b95ba6f5640}' : gettext('Mozilla'),
@ -111,6 +122,12 @@ var csv_keys = {
// L10n: both {0} and {1} are dates in YYYY-MM-DD format.
gettext("Downloads and Daily Users from {0} to {1}")
],
"app_overview" : [
// L10n: {0} is an integer.
gettext("Installs and Daily Users, last {0} days"),
// L10n: both {0} and {1} are dates in YYYY-MM-DD format.
gettext("Installs and Daily Users from {0} to {1}")
],
"downloads" : [
// L10n: {0} is an integer.
gettext("Downloads, last {0} days"),
@ -224,6 +241,18 @@ var csv_keys = {
gettext("Ratings, last {0} days"),
// L10n: both {0} and {1} are dates in YYYY-MM-DD format.
gettext("Ratings from {0} to {1}")
],
"sales" : [
// L10n: {0} is an integer.
gettext("Sales, last {0} days"),
// L10n: both {0} and {1} are dates in YYYY-MM-DD format.
gettext("Sales from {0} to {1}")
],
"installs" : [
// L10n: {0} is an integer.
gettext("Installs, last {0} days"),
// L10n: both {0} and {1} are dates in YYYY-MM-DD format.
gettext("Installs from {0} to {1}")
]
},
aggregateLabel: {

Просмотреть файл

@ -24,7 +24,8 @@ z.StatsManager = (function() {
pendingFetches = 0,
siteEventsEnabled = $('body').hasClass('waffle-site-events'),
writeInterval = false,
lookup = {};
lookup = {},
msDay = 24 * 60 * 60 * 1000; // One day in milliseconds.
// NaN is a poor choice for a storage key
if (isNaN(addonId)) addonId = 'globalstats';
@ -110,7 +111,7 @@ z.StatsManager = (function() {
// On custom ranges request a range greater by 1 day. (bug 737910)
if (currentView.range.custom && typeof currentView.range.end == 'object') {
currentView.range.end = new Date(currentView.range.end.getTime() + (24 * 60 * 60 * 1000));
currentView.range.end = new Date(currentView.range.end.getTime() + msDay);
}
// Fetch the data from the server or storage, and notify other components.

Просмотреть файл

@ -3,7 +3,7 @@
$(function() {
"use strict";
// Modify the URL when the page state changes, if the browser supports pushSate.
// Modify the URL when the page state changes, if the browser supports pushState.
if (z.capabilities.replaceState) {
$(window).bind('changeview', function(e, view) {
var queryParams = {},

Просмотреть файл

@ -10,7 +10,8 @@
var href = this.getAttribute('href');
if (e.metaKey || e.ctrlKey || e.button !== 0) return;
if (!href || href.substr(0,4) == 'http' || href === '#' ||
href.indexOf('/developers/') !== -1) {
href.indexOf('/developers/') !== -1 ||
href.indexOf('/statistics/') !== -1) {
return;
}
e.preventDefault();

175
media/js/mkt/modal.js Normal file
Просмотреть файл

@ -0,0 +1,175 @@
// makes an element into a modal.
// click_target defines the element/elements that trigger the modal.
// currently presumes the given element uses the '.modal' style
// o takes the following optional fields:
// callback: a function to run before displaying the modal. Returning
// false will cancel the modal.
// container: if set the modal will be appended to the container before
// being displayed.
// width: the width of the modal.
// delegate: delegates the click handling of the click_target to the
// specified parent element.
// hideme: defaults to true; if set to false, modal will not be hidden
// when the user clicks outside of it.
// emptyme: defaults to false; if set to true, modal will be cleared
// after it is hidden.
// deleteme: defaults to false; if set to true, popup will be deleted
// after it is hidden.
// close: defaults to false; if set to true, modal will have a
// close button
// note: all options may be overridden and modified by returning them in an
// object from the callback.
//
// If you want to close all existing modals, use:
// $('.modal').trigger('close');
$.fn.modal = function(click_target, o) {
o = o || {};
var $ct = $(click_target),
$modal = this,
forcedOffset = 60; //distance from top of the window
$modal.o = $.extend({
delegate: false,
callback: false,
onresize: function(){$modal.setPos();},
hideme: true,
emptyme: false,
deleteme: false,
offset: {},
width: 450
}, o);
$modal.setWidth = function(w) {
$modal.css({width: w});
return $modal;
};
$modal.setPos = function(offset) {
offset = offset || $modal.o.offset;
$modal.detach().appendTo("body");
var toX = ($(window).width() - $modal.outerWidth()) / 2,
toY = $(window).scrollTop() + forcedOffset;
$modal.css({
'left': toX,
'top': toY + 'px',
'right': 'inherit',
'bottom': 'inherit',
'position': 'absolute'
});
return $modal;
};
$modal.hideMe = function() {
var p = $modal.o;
$modal.hide();
$modal.unbind();
$modal.undelegate();
$(document.body).unbind('click newmodal', $modal.hider);
$(window).unbind('keydown.lightboxDismiss');
$(window).bind('resize', p.onresize);
$('.modal-overlay').remove();
return $modal;
};
function handler(e) {
e.preventDefault();
var resp = o.callback ? (o.callback.call($modal, {
click_target: this,
evt: e
})) !== false : true;
$modal.o = $.extend({click_target: this}, $modal.o, resp);
if (resp) {
$('.modal').trigger('close'); // We don't want two!
$modal.render();
}
}
$modal.render = function() {
var p = $modal.o;
$modal.hider = makeBlurHideCallback($modal);
if (p.hideme) {
try {
setTimeout(function(){
$(document.body).bind('click modal', $modal.hider);
}, 0);
} catch (err) {
// TODO(Kumar) handle this more gracefully. See bug 701221.
if (typeof console !== 'undefined') {
console.error('Could not close modal:', err);
}
}
}
if (p.close) {
var close = $("<a>", {'class': 'close', 'text': 'X'});
$modal.append(close);
}
$('.popup').hide();
$modal.delegate('.close', 'click', function(e) {
e.preventDefault();
$modal.trigger('close');
});
$modal.bind('close', function(e) {
if (p.emptyme) {
$modal.empty();
}
if (p.deleteme) {
$modal.remove();
}
e.preventDefault();
$modal.hideMe();
});
$ct.trigger("modal_show", [$modal]);
if (p.container && p.container.length)
$modal.detach().appendTo(p.container);
$('<div class="modal-overlay"></div>').appendTo('body');
$modal.setPos();
setTimeout(function(){
$modal.show();
}, 0);
$(window).bind('resize', p.onresize)
.bind('keydown.lightboxDismiss', function(e) {
if (e.which == 27) {
$modal.hideMe();
}
});
return $modal;
};
if ($modal.o.delegate) {
$($modal.o.delegate).delegate(click_target, "click", handler);
} else {
$ct.click(handler);
}
$modal.setWidth($modal.o.width);
return $modal;
};
// returns an event handler that will hide/unbind an element when a click is
// registered outside itself.
function makeBlurHideCallback(el) {
var hider = function(e) {
_root = el.get(0);
// Bail if the click was somewhere on the popup.
if (e) {
if (e.type == 'click' &&
_root == e.target ||
_.indexOf($(e.target).parents(), _root) != -1) {
return;
}
}
el.hideMe();
if (el.o.emptyme) {
el.empty();
}
if (el.o.deleteme) {
el.remove();
}
};
return hider;
}

Просмотреть файл

@ -133,6 +133,9 @@ CSS = {
'css/impala/localizers.less',
'css/mkt/in-app-payments.less',
),
'mkt/stats': (
'css/mkt/stats.less',
),
'marketplace-experiments': (
'marketplace-experiments/css/reset.less',
'marketplace-experiments/css/site.less',
@ -223,6 +226,22 @@ JS = {
# Account settings.
'js/mkt/account.js',
),
'mkt/stats': (
'js/zamboni/storage.js',
'js/mkt/modal.js',
'js/lib/jquery-datepicker.js',
'js/lib/highcharts.src.js',
'js/impala/stats/csv_keys.js',
'js/impala/stats/helpers.js',
'js/impala/stats/dateutils.js',
'js/impala/stats/manager.js',
'js/impala/stats/controls.js',
'js/impala/stats/overview.js',
'js/impala/stats/topchart.js',
'js/impala/stats/chart.js',
'js/impala/stats/table.js',
'js/impala/stats/stats.js',
),
'marketplace-experiments': (
'js/marketplace-experiments/jquery-1.7.1.min.js',
'js/marketplace-experiments/slider.js',

Просмотреть файл

@ -11,6 +11,9 @@ urlpatterns = patterns('',
# Submission.
('^purchase/', include('mkt.purchase.urls')),
# Statistics
('^statistics/', include('mkt.stats.urls')),
# TODO: Port abuse.
url('^abuse$', addons.views.report_abuse, name='detail.abuse'),
)

Просмотреть файл

@ -50,6 +50,7 @@ INSTALLED_APPS += (
'mkt.purchase',
'mkt.reviewers',
'mkt.search',
'mkt.stats',
'mkt.submit',
'mkt.support',
'mkt.webapps',

0
mkt/stats/__init__.py Normal file
Просмотреть файл

Просмотреть файл

@ -0,0 +1,25 @@
<nav id="side-nav" class="report-menu">
<ul>
<li data-report="overview" data-layout="overview">
<a href="{{ url('mkt.stats.overview', addon.app_slug) }}">
{{ _('Overview') }}
</a>
</li>
{# TODO: Sales stats should only show up for premium apps. #}
<li data-report="sales">
<a href="{{ url('mkt.stats.sales', addon.app_slug) }}">
{{ _('Sales') }}
</a>
</li>
<li data-report="installs">
<a href="{{ url('mkt.stats.installs', addon.app_slug) }}">
{{ _('Installs') }}
</a>
</li>
<li data-report="usage">
<a href="{{ url('mkt.stats.usage', addon.app_slug) }}">
{{ _('Usage') }}
</a>
</li>
</ul>
</nav>

Просмотреть файл

@ -0,0 +1,66 @@
{% extends "appstats/stats.html" %}
{% block csvtable %}{% endblock %}
{% block stats %}
<section class="island two-up">
<div>
<a href="downloads/">
{{ _('<b>{0}</b> Installs')|
f(addon.total_downloads|numberfmt)|safe }}
</a>
<small id="downloads-in-range">{{ _('Loading...') }}</small>
</div>
<div>
<a href="usage/">
{{ _('<b>{0}</b> Average Daily Users')|
f(addon.average_daily_users|numberfmt)|safe }}
</a>
<small id="users-in-range">{{ _('Loading...') }}</small>
</div>
</section>
<div class="toplists hidden">
<div class="toplist">
<div class="island statbox">
<h2>{{ _('Top Apps') }}</h3>
<div class="piechart"></div>
<table data-metric="apps">
</table>
<a class="more" href="usage/applications/">
{{ _('See more apps&hellip;') }}
</a>
<div class="no-data-overlay">
<p>{{ _('No data available.') }}</p>
</div>
</div>
</div>
<div class="toplist">
<div class="island statbox">
<h2>{{ _('Top Languages') }}</h3>
<div class="piechart"></div>
<table data-metric="locales">
</table>
<a class="more" href="usage/languages/">
{{ _('See more languages&hellip;') }}
</a>
<div class="no-data-overlay">
<p>{{ _('No data available.') }}</p>
</div>
</div>
</div>
<div class="toplist">
<div class="island statbox">
<h2>{{ _('Top Devices') }}</h3>
<div class="piechart"></div>
<table data-metric="os">
</table>
<a class="more" href="usage/os/">
{{ _('See more devices&hellip;') }}
</a>
<div class="no-data-overlay">
<p>{{ _('No data available.') }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

Просмотреть файл

@ -0,0 +1,5 @@
{% extends "appstats/stats.html" %}
{% block csvtitle %}
<h2>{{_('Installs by Date')}}<a id="export_data" href=''>{{_('Export as CSV')}}</a></h2>
{% endblock %}

Просмотреть файл

@ -0,0 +1,5 @@
{% extends "appstats/stats.html" %}
{% block csvtitle %}
<h2>{{_('Sales by Date')}}<a id="export_data" href=''>{{_('Export as CSV')}}</a></h2>
{% endblock %}

Просмотреть файл

@ -0,0 +1,5 @@
{% extends "appstats/stats.html" %}
{% block csvtitle %}
<h2>{{_('Usage by Date')}}<a id="export_data" href=''>{{_('Export as CSV')}}</a></h2>
{% endblock %}

Просмотреть файл

@ -0,0 +1,183 @@
{% extends 'mkt/base.html' %}
{% set range = view.range %}
{% block bodyclass %}
statistics
{{ 'waffle-site-events' if waffle.switch('site-events') else '' }}
{% endblock %}
{% block extrahead %}
{{ css('mkt/stats') }}
<link rel="stylesheet"
href="{{ media('css/zamboni/jquery-ui/custom-1.7.2.css') }}">
{% endblock %}
{% block title %}
{# L10n: {0} is the app name #}
{{ _('{0} · Statistics Dashboard')|f(addon.name) }}
{% endblock %}
{% block content %}
<section id="stats">
<header class="c">
{{ impala_breadcrumbs([(addon.type_url(), amo.ADDON_TYPES[addon.type]),
(addon.get_url_path(), addon.name),
(link, _('Statistics'))]) }}
<hgroup class="c">
<h1 class="addon"{{ addon.name|locale_html }}>
{# L10n: {0} is an add-on name #}
{{ _('Statistics for {0}')|f(addon.name) }}
</h1>
</hgroup>
<div class="criteria island">
<ul>
<li>{{ _('Controls:') }}</li>
<li>
<a id="chart-zoomout" class="inactive" href="#">
{{ _('reset zoom') }}
</a>
</li>
</ul>
</div>
<div class="criteria group island">
<ul>
<li>{{ _('Group by:') }}</li>
<li data-group="day">
<a href="#">{{ _('day') }}</a>
</li>
<li data-group="week">
<a href="#">{{ _('week') }}</a>
</li>
<li data-group="month">
<a href="#">{{ _('month') }}</a>
</li>
</ul>
</div>
<div class="criteria range island">
<ul>
<li>{{ _('For last:') }}</li>
<li data-range="7 days"
{% if range == '7' %}class="selected"{% endif %}>
<a href="#">{{ _('7 days') }}</a></li>
<li data-range="30 days"
{% if range == '30' %}class="selected"{% endif %}>
<a href="#">{{ _('30 days') }}</a></li>
<li data-range="90 days"
{% if range == '90' %}class="selected"{% endif %}>
<a href="#">{{ _('90 days') }}</a></li>
<li data-range="365 days"
{% if range == '365' %}class="selected"{% endif %}>
<a href="#">{{ _('365 days') }}</a></li>
<li data-range="custom"
{% if range == 'custom' %}class="selected"{% endif %}>
<a id="custom-date-range" href="#">{{ _('Custom&hellip;') }}</a></li>
</ul>
</div>
</header>
<div id="lm" class="loadmessage">
<span>{{ _('Loading the latest data&hellip;') }}</span>
</div>
{# Initial stats will be `installs` only. Uncomment when others are done.
<div class="secondary">
{{ report_menu(request, report, obj=addon) }}
{% block stats_note_link %}{% endblock %}
<pre id="dbgout"></pre>
</div>
#}
<div class="primary statistics"
{% if addon %}
data-min-date="{{ addon.created|isotime }}"
data-addon_id="{{ addon.id }}"
{% endif %}
data-report="{{ report }}"
{% if view.last %}
data-range="{{ view.last }}"
{% endif %}
{% if view.start and view.end %}
data-range="custom"
data-start_date="{{ view.start }}"
data-end_date="{{ view.end }}"
{% endif %}
data-base_url="{{ stats_base_url }}">
<div class="island chart">
<div id="head-chart">
</div>
<div class="no-data-overlay">
<p>{{ _('No data available.') }}</p>
</div>
</div>
{% block stats %}
{% endblock %}
{% block csvtable %}
<div class="island">
{% block csvtitle %}{% endblock %}
<div class="tabular csv-table">
<div class="table-box">
<table>
<thead>
</thead>
</table>
</div>
<footer>
<nav class="paginator c">
<p class="range">
</p>
<p class="rel">
<a href="#"
class="button prev disabled">
&#x25C2; {{ _('Previous') }}</a>
<a href="#"
class="button next">
{{ _('Next') }} &#x25B8;</a>
</p>
</nav>
</footer>
</div>
</div>
{% endblock %}
<div id="stats-permissions">
{% if addon.public_stats %}
<p>{{ _('This dashboard is currently <b>public</b>.') }}</p>
{% if request.check_ownership(addon) %}
<p>{{ _('Contribution stats are currently <b>private</b>.') }}</p>
{% endif %}
{% else %}
<p>{{ _('This dashboard is currently <b>private</b>.') }}</p>
{% endif %}
{% if request.check_ownership(addon) %}
<p>
<a href="{{ addon.get_dev_url() }}#edit-addon-technical">
{{ _('Change settings.') }}
</a>
</p>
{% endif %}
</div>
<div class="hidden">
<div id="fieldMenuPopup" class="popup">
<form id="fieldMenu">
<ul id="fieldList">
</ul>
</form>
</div>
</div>
</div>
<div id="popup-container">
{% include 'stats/popup.html' %}
</div>
</section>
{% endblock %}
{% block js %}
<!--[if IE]>
<script
src="{{ media('js/lib/excanvas.compiled.js') }}">
</script>
<![endif]-->
{{ js('mkt/stats') }}
{% endblock %}

31
mkt/stats/urls.py Normal file
Просмотреть файл

@ -0,0 +1,31 @@
from django.conf.urls.defaults import include, patterns, url
import addons.views
from . import views
from stats.urls import series_re
series = dict((type, '%s-%s' % (type, series_re)) for type in views.SERIES)
urlpatterns = patterns('',
# This will eventually be kwargs={'report': 'app_overview'}
url('^$', views.stats_report, name='mkt.stats.overview',
kwargs={'report': 'installs'}),
url('^sales/$', views.stats_report, name='mkt.stats.sales',
kwargs={'report': 'sales'}),
url('^installs/$', views.stats_report, name='mkt.stats.installs',
kwargs={'report': 'installs'}),
url('^usage/$', views.stats_report, name='mkt.stats.usage',
kwargs={'report': 'usage'}),
# time series URLs following this pattern:
# /app/{app_slug}/statistics/{series}-{group}-{start}-{end}.{format}
url(series['app_overview'], views.overview_series,
name='mkt.stats.overview_series'),
url(series['sales'], views.sales_series,
name='mkt.stats.sales_series'),
url(series['installs'], views.installs_series,
name='mkt.stats.installs_series'),
url(series['usage'], views.usage_series,
name='mkt.stats.usage_series'),
)

123
mkt/stats/views.py Normal file
Просмотреть файл

@ -0,0 +1,123 @@
import time
from datetime import date, timedelta
import jingo
from addons.decorators import addon_view, addon_view_factory
from addons.models import Addon
import amo
from amo.decorators import json_view
from amo.urlresolvers import reverse
# Reuse Potch's box of magic.
from stats.models import DownloadCount
from stats.views import (check_series_params_or_404, check_stats_permission,
daterange, get_report_view, get_series, render_json,
SERIES_GROUPS, SERIES_GROUPS_DATE, SERIES_FORMATS)
# Most of these are not yet available.
SERIES = ('active', 'devices', 'installs', 'app_overview', 'referrers', 'sales',
'usage')
@addon_view_factory(Addon.objects.valid)
def stats_report(request, addon, report):
check_stats_permission(request, addon)
stats_base_url = reverse('mkt.stats.overview', args=[addon.app_slug])
view = get_report_view(request)
return jingo.render(request, 'appstats/reports/%s.html' % report,
{'addon': addon,
'report': report,
'view': view,
'stats_base_url': stats_base_url})
#TODO: This view will require some complex JS logic similar to apps/stats.
@addon_view
def overview_series(request, addon, group, start, end, format):
"""Combines installs_series and usage_series into one payload."""
date_range = check_series_params_or_404(group, start, end, format)
check_stats_permission(request, addon)
dls = get_series(DownloadCount, addon=addon.id, date__range=date_range)
# Uncomment the line below to return fake stats.
return fake_app_stats(request, addon, group, start, end, format)
return render_json(request, addon, dls)
#TODO: Real stats data needs to be plugged in.
@addon_view
def sales_series(request, addon, group, start, end, format):
date_range = check_series_params_or_404(group, start, end, format)
check_stats_permission(request, addon)
series = get_series(DownloadCount, addon=addon.id, date__range=date_range)
# Uncomment the line below to return fake stats.
return fake_app_stats(request, addon, group, start, end, format)
if format == 'csv':
return render_csv(request, addon, series, ['date', 'count'])
elif format == 'json':
return render_json(request, addon, series)
#TODO: Real stats data needs to be plugged in.
@addon_view
def installs_series(request, addon, group, start, end, format):
"""Generate install counts grouped by ``group`` in ``format``."""
date_range = check_series_params_or_404(group, start, end, format)
check_stats_permission(request, addon)
series = get_series(DownloadCount, addon=addon.id, date__range=date_range)
# Uncomment the line below to return fake stats.
return fake_app_stats(request, addon, group, start, end, format)
if format == 'csv':
return render_csv(request, addon, series, ['date', 'count'])
elif format == 'json':
return render_json(request, addon, series)
#TODO: Real stats data needs to be plugged in.
@addon_view
def usage_series(request, addon, group, start, end, format):
date_range = check_series_params_or_404(group, start, end, format)
check_stats_permission(request, addon)
series = get_series(DownloadCount, addon=addon.id, date__range=date_range)
# Uncomment the line below to return fake stats.
return fake_app_stats(request, addon, group, start, end, format)
if format == 'csv':
return render_csv(request, addon, series, ['date', 'count'])
elif format == 'json':
return render_json(request, addon, series)
@json_view
def fake_app_stats(request, addon, group, start, end, format):
from time import strftime
from math import sin, floor
start, end = check_series_params_or_404(group, start, end, format)
faked = []
val = 0
for single_date in daterange(start, end):
isodate = strftime("%Y-%m-%d", single_date.timetuple())
faked.append({
'date': isodate,
'count': floor(200 + 50 * sin(val + 1)),
'data': {
'installs': floor(200 + 50 * sin(2 * val + 2)),
'usage': floor(200 + 50 * sin(3 * val + 3)),
'sales': floor(200 + 50 * sin(4 * val + 4)),
#'device': floor(200 + 50 * sin(5 * val + 5)),
}})
val += .01
return faked

Просмотреть файл

@ -97,5 +97,6 @@
})();
</script>
{% endblock %}
{% block js %}{% endblock %}
</body>
</html>

Просмотреть файл

@ -34,6 +34,16 @@ urlpatterns = patterns('',
# In-app payments.
('^inapp-pay/', include('mkt.inapp_pay.urls')),
# Site events data.
url('^statistics/events-(?P<start>\d{8})-(?P<end>\d{8}).json$',
'stats.views.site_events', name='amo.site_events'),
# Site statistics that we are going to catch, the rest will fall through.
url('^statistics/', include('stats.urls')),
# Fall through for any URLs not matched above stats dashboard.
url('^statistics/', lambda r: redirect('/'), name='statistics.dashboard'),
# Support (e.g., refunds, FAQs).
('^support/', include('mkt.support.urls')),