addons-server/apps/stats/views.py

300 строки
11 KiB
Python

import time
from types import GeneratorType
from datetime import date, datetime
from django import http
from django.shortcuts import get_object_or_404
from django.utils import simplejson
from django.core.serializers.json import DjangoJSONEncoder
from django.core.exceptions import PermissionDenied
from django.views.decorators.cache import cache_control
import jingo
from access import acl
from addons.models import Addon
from amo.urlresolvers import reverse
import unicode_csv
from .db import DayAvg, Avg
from .models import DownloadCount, UpdateCount, Contribution
from .utils import csv_prep, csv_dynamic_prep
SERIES_GROUPS = ('day', 'week', 'month')
SERIES_FORMATS = ('json', 'csv')
SERIES = ('downloads', 'usage', 'contributions',
'sources', 'os', 'locales', 'statuses', 'versions', 'apps')
def downloads_series(request, addon_id, group, start, end, format):
"""Generate download counts grouped by ``group`` in ``format``."""
start_date, end_date, addon = check_series_params_or_404(
addon_id, group, start, end, format)
check_stats_permission(request, addon)
# resultkey to fieldname map - stored as a list to maintain order for csv
fields = [('date', 'start'), ('count', 'count')]
qs = DownloadCount.stats.filter(addon=addon_id,
date__range=(start_date, end_date))
gen = qs.period_summary(group, **dict(fields))
if format == 'csv':
gen, headings = csv_prep(gen, fields)
return render_csv(request, addon, gen, headings)
elif format == 'json':
return render_json(request, addon, gen)
def usage_series(request, addon_id, group, start, end, format):
"""Generate ADU counts grouped by ``group`` in ``format``."""
start_date, end_date, addon = check_series_params_or_404(
addon_id, group, start, end, format)
check_stats_permission(request, addon)
# resultkey to fieldname map - stored as a list to maintain order for csv
fields = [('date', 'start'), ('count', DayAvg('count'))]
qs = UpdateCount.stats.filter(addon=addon_id,
date__range=(start_date, end_date))
gen = qs.period_summary(group, **dict(fields))
if format == 'csv':
gen, headings = csv_prep(gen, fields)
return render_csv(request, addon, gen, headings)
elif format == 'json':
return render_json(request, addon, gen)
def contributions_series(request, addon_id, group, start, end, format):
"""Generate summarized contributions grouped by ``group`` in ``format``."""
start_date, end_date, addon = check_series_params_or_404(
addon_id, group, start, end, format)
check_stats_permission(request, addon, for_contributions=True)
qs = addon_contributions_queryset(addon, start_date, end_date)
# Note that average is per contribution and not per day
fields = [('date', 'start'), ('total', 'amount'), ('count', 'row_count'),
('average', Avg('amount'))]
gen = qs.period_summary(group, **dict(fields))
if format == 'csv':
gen, headings = csv_prep(gen, fields, precision='0.01')
return render_csv(request, addon, gen, headings)
elif format == 'json':
return render_json(request, addon, gen)
def contributions_detail(request, addon_id, start, end, format):
"""Generate detailed contributions in ``format``."""
# This view doesn't do grouping, but we can leverage our series parameter
# checker by passing in a valid group value.
start_date, end_date, addon = check_series_params_or_404(
addon_id, 'day', start, end, format)
check_stats_permission(request, addon, for_contributions=True)
qs = addon_contributions_queryset(addon, start_date, end_date)
def property_lookup_gen(qs, fields):
for obj in qs:
yield dict((k, getattr(obj, f, None)) for k, f in fields)
fields = [('date', 'date'), ('amount', 'amount'),
('requested', 'suggested_amount'),
('contributor', 'contributor'),
('email', 'email'), ('comment', 'comment')]
gen = property_lookup_gen(qs, fields)
if format == 'csv':
gen, headings = csv_prep(gen, fields, precision='0.01')
return render_csv(request, addon, gen, headings)
elif format == 'json':
return render_json(request, addon, gen)
def sources_series(request, addon_id, group, start, end, format):
"""Generate download source breakdown."""
start_date, end_date, addon = check_series_params_or_404(
addon_id, group, start, end, format)
check_stats_permission(request, addon)
# resultkey to fieldname map - stored as a list to maintain order for csv
fields = [('date', 'start'), ('count', 'count'), ('sources', 'sources')]
qs = DownloadCount.stats.filter(addon=addon_id,
date__range=(start_date, end_date))
gen = qs.period_summary(group, **dict(fields))
if format == 'csv':
gen, headings = csv_dynamic_prep(gen, qs, fields, 'count', 'sources')
return render_csv(request, addon, gen, headings)
elif format == 'json':
return render_json(request, addon, gen)
def usage_breakdown_series(request, addon_id, group,
start, end, format, field):
"""Generate ADU breakdown of ``field``."""
start_date, end_date, addon = check_series_params_or_404(
addon_id, group, start, end, format)
check_stats_permission(request, addon)
# resultkey to fieldname map - stored as a list to maintain order for csv
# Use DayAvg so days with 0 rows affect the calculation.
fields = [('date', 'start'), ('count', DayAvg('count')),
(field, DayAvg(field))]
qs = UpdateCount.stats.filter(addon=addon_id,
date__range=(start_date, end_date))
gen = qs.period_summary(group, **dict(fields))
if format == 'csv':
gen, headings = csv_dynamic_prep(gen, qs, fields,
'count', field)
return render_csv(request, addon, gen, headings)
elif format == 'json':
return render_json(request, addon, gen)
def check_series_params_or_404(addon_id, group, start, end, format):
"""Check common series parameters."""
if (group not in SERIES_GROUPS) or (format not in SERIES_FORMATS):
raise http.Http404
(start_date, end_date) = get_daterange_or_404(start, end)
addon = get_object_or_404(Addon, id=addon_id)
return (start_date, end_date, addon)
def check_stats_permission(request, addon, for_contributions=False):
"""Check if user is allowed to view stats for ``addon``.
Raises PermissionDenied if user is not allowed.
"""
if for_contributions or not addon.public_stats:
# only authenticated admins and authors
if (request.user.is_authenticated() and (
acl.action_allowed(request, 'Admin', 'ViewAnyStats') or
addon.has_author(request.amo_user))):
return
elif addon.public_stats:
# non-contributions, public: everybody can view
return
raise PermissionDenied
def stats_report(request, addon_id, report):
addon = get_object_or_404(Addon.objects.valid(), id=addon_id)
check_stats_permission(request, addon)
stats_base_url = reverse('stats.overview', args=[addon.id])
view = get_report_view(request)
return jingo.render(request, 'stats/%s.html' % report,
{'addon': addon,
'report': report,
'view': view,
'stats_base_url': stats_base_url})
def get_report_view(request):
"""Parse and validate a pair of YYYMMDD date strings."""
if ('start' in request.GET and
'end' in request.GET):
try:
start = request.GET.get('start')
end = request.GET.get('end')
assert len(start) == 8
assert len(end) == 8
s_year = int(start[0:4])
s_month = int(start[4:6])
s_day = int(start[6:8])
e_year = int(end[0:4])
e_month = int(end[4:6])
e_day = int(end[6:8])
date(s_year, s_month, s_day)
date(e_year, e_month, e_day)
return {'range': 'custom',
'start': start,
'end': end}
except (KeyError, AssertionError, ValueError):
pass
if 'last' in request.GET:
daterange = request.GET.get('last')
return {'range': daterange, 'last': daterange}
else:
return {'range': '30', 'last': '30'}
def get_daterange_or_404(start, end):
"""Parse and validate a pair of YYYMMDD date strings."""
try:
assert len(start) == 8
assert len(end) == 8
s_year = int(start[0:4])
s_month = int(start[4:6])
s_day = int(start[6:8])
e_year = int(end[0:4])
e_month = int(end[4:6])
e_day = int(end[6:8])
start_date = date(s_year, s_month, s_day)
end_date = date(e_year, e_month, e_day)
except (AssertionError, ValueError):
raise http.Http404
return (start_date, end_date)
def addon_contributions_queryset(addon, start_date, end_date):
"""Return a Contribution queryset common to all contribution views."""
# Contribution.created is a datetime.
# Make sure we include all on the last day of the range.
if not isinstance(end_date, datetime):
end_date = datetime(end_date.year, end_date.month,
end_date.day, 23, 59, 59)
return Contribution.stats.filter(addon=addon,
transaction_id__isnull=False,
amount__gt=0,
created__range=(start_date, end_date))
# 30 days in seconds:
thirty_days = 60 * 60 * 24 * 30
@cache_control(max_age=thirty_days)
def render_csv(request, addon, stats, fields):
"""Render a stats series in CSV."""
# Start with a header from the template.
ts = time.strftime('%c %z')
response = jingo.render(request, 'stats/csv_header.txt',
{'addon': addon, 'fields': fields,
'timestamp': ts})
# For remora compatibility, reverse the output so oldest data
# is first.
# XXX: The list() performance penalty here might be big enough to
# consider changing the sort order at lower levels.
writer = unicode_csv.UnicodeWriter(response)
for row in reversed(list(stats)):
writer.writerow(row)
response['Content-Type'] = 'text/plain; charset=utf-8'
return response
@cache_control(max_age=thirty_days)
def render_json(request, addon, stats):
"""Render a stats series in JSON."""
response = http.HttpResponse(mimetype='text/json')
# XXX: Subclass DjangoJSONEncoder to handle generators.
if isinstance(stats, GeneratorType):
stats = list(stats)
# Django's encoder supports date and datetime.
simplejson.dump(stats, response, cls=DjangoJSONEncoder)
return response