app stats implementation (bug 695236)
This commit is contained in:
Родитель
e03b1cf9f2
Коммит
b0ccd4e11f
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,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…') }}
|
||||
</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…') }}
|
||||
</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…') }}
|
||||
</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…') }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="lm" class="loadmessage">
|
||||
<span>{{ _('Loading the latest data…') }}</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">
|
||||
◂ {{ _('Previous') }}</a>
|
||||
<a href="#"
|
||||
class="button next">
|
||||
{{ _('Next') }} ▸</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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -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>
|
||||
|
|
10
mkt/urls.py
10
mkt/urls.py
|
@ -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')),
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче