Files
notifications-admin/app/main/views/dashboard.py
Chris Hill-Scott 2a502753a4 Filter and navigate usage by financial year
Right now we tell people that the usage page is for the current
financial year. This is a lie – it’s for all time.

So this commit calls through to the API to get the stats for (by
default) the current financial year.

We already do this for the monthly breakdown, this just does the same
thing for the yearly totals.

It also adds navigation to show the data for other financial years:
- previous so you can go back and see your usage and verify that the
  bill you’re about to pay is correct
- next so that you can check what your SMS allowance is going to be
  before you actually get into it
2017-02-06 12:25:48 +00:00

272 lines
8.4 KiB
Python

from datetime import datetime, timedelta
import dateutil
from flask import (
render_template,
url_for,
session,
jsonify,
current_app,
request,
abort
)
from flask_login import login_required
from app.main import main
from app import (
job_api_client,
service_api_client,
template_statistics_client
)
from app.statistics_utils import get_formatted_percentage, add_rate_to_job
from app.utils import user_has_permissions, get_current_financial_year
# This is a placeholder view method to be replaced
# when product team makes decision about how/what/when
# to view history
@main.route("/services/<service_id>/history")
@login_required
def temp_service_history(service_id):
data = service_api_client.get_service_history(service_id)['data']
return render_template('views/temp-history.html',
services=data['service_history'],
api_keys=data['api_key_history'],
events=data['events'])
@main.route("/services/<service_id>/dashboard")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def service_dashboard(service_id):
if session.get('invited_user'):
session.pop('invited_user', None)
session['service_id'] = service_id
return render_template(
'views/dashboard/dashboard.html',
updates_url=url_for(".service_dashboard_updates", service_id=service_id),
templates=service_api_client.get_service_templates(service_id)['data'],
partials=get_dashboard_partials(service_id)
)
@main.route("/services/<service_id>/dashboard.json")
@user_has_permissions('view_activity', admin_override=True)
def service_dashboard_updates(service_id):
return jsonify(**get_dashboard_partials(service_id))
@main.route("/services/<service_id>/template-activity")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def template_history(service_id):
template_statistics = aggregate_usage(
template_statistics_client.get_template_statistics_for_service(service_id)
)
return render_template(
'views/dashboard/all-template-statistics.html',
template_statistics=template_statistics,
most_used_template_count=max(
[row['count'] for row in template_statistics] or [0]
)
)
@main.route("/services/<service_id>/usage")
@login_required
@user_has_permissions('manage_settings', admin_override=True)
def usage(service_id):
current_financial_year = get_current_financial_year()
try:
year = int(request.args.get('year', current_financial_year))
except ValueError:
abort(404)
return render_template(
'views/usage.html',
months=list(get_free_paid_breakdown_for_billable_units(
year, service_api_client.get_billable_units(service_id, year)
)),
selected_year=year,
years=[
(
'financial year',
year,
url_for('.usage', service_id=service_id, year=year),
'{} to {}'.format(year, year + 1),
) for year in range(current_financial_year - 1, current_financial_year + 2)
],
**calculate_usage(service_api_client.get_service_usage(service_id, year)['data'])
)
@main.route("/services/<service_id>/weekly")
@login_required
@user_has_permissions('manage_settings', admin_override=True)
def weekly(service_id):
stats = service_api_client.get_weekly_notification_stats(service_id)['data']
return render_template(
'views/weekly.html',
days=format_weekly_stats_to_list(stats),
now=datetime.utcnow()
)
def aggregate_usage(template_statistics):
return sorted(
template_statistics,
key=lambda template_statistic: template_statistic['count'],
reverse=True
)
def get_dashboard_partials(service_id):
# all but scheduled and cancelled
statuses_to_display = job_api_client.JOB_STATUSES - {'scheduled', 'cancelled'}
template_statistics = aggregate_usage(
template_statistics_client.get_template_statistics_for_service(service_id, limit_days=7)
)
scheduled_jobs = sorted(
job_api_client.get_jobs(service_id, statuses=['scheduled'])['data'],
key=lambda job: job['scheduled_for']
)
immediate_jobs = [
add_rate_to_job(job)
for job in job_api_client.get_jobs(service_id, limit_days=7, statuses=statuses_to_display)['data']
]
service = service_api_client.get_detailed_service(service_id)
return {
'upcoming': render_template(
'views/dashboard/_upcoming.html',
scheduled_jobs=scheduled_jobs
),
'totals': render_template(
'views/dashboard/_totals.html',
service_id=service_id,
statistics=get_dashboard_totals(service['data']['statistics'])
),
'template-statistics': render_template(
'views/dashboard/template-statistics.html',
template_statistics=template_statistics,
most_used_template_count=max(
[row['count'] for row in template_statistics] or [0]
),
),
'has_template_statistics': bool(template_statistics),
'jobs': render_template(
'views/dashboard/_jobs.html',
jobs=immediate_jobs
),
'has_jobs': bool(immediate_jobs),
'usage': render_template(
'views/dashboard/_usage.html',
**calculate_usage(service_api_client.get_service_usage(
service_id,
get_current_financial_year(),
)['data'])
),
}
def get_dashboard_totals(statistics):
for msg_type in statistics.values():
msg_type['failed_percentage'] = get_formatted_percentage(msg_type['failed'], msg_type['requested'])
msg_type['show_warning'] = float(msg_type['failed_percentage']) > 3
return statistics
def calculate_usage(usage):
# TODO: Don't hardcode these - get em from the API
sms_free_allowance = 250000
sms_rate = 0.0165
sms_sent = usage.get('sms_count', 0)
emails_sent = usage.get('email_count', 0)
return {
'emails_sent': emails_sent,
'sms_free_allowance': sms_free_allowance,
'sms_sent': sms_sent,
'sms_allowance_remaining': max(0, (sms_free_allowance - sms_sent)),
'sms_chargeable': max(0, sms_sent - sms_free_allowance),
'sms_rate': sms_rate
}
def format_weekly_stats_to_list(historical_stats):
out = []
for week, weekly_stats in historical_stats.items():
for stats in weekly_stats.values():
stats['failure_rate'] = get_formatted_percentage(stats['failed'], stats['requested'])
week_start = dateutil.parser.parse(week)
week_end = week_start + timedelta(days=6)
weekly_stats.update({
'week_start': week,
'week_end': week_end.date().isoformat(),
'week_end_datetime': week_end,
})
out.append(weekly_stats)
return sorted(out, key=lambda x: x['week_start'], reverse=True)
def get_months_for_financial_year(year):
return [
month.strftime('%B')
for month in (
get_months_for_year(4, 13, year) +
get_months_for_year(1, 4, year + 1)
)
if month < datetime.now()
]
def get_months_for_year(start, end, year):
return [datetime(year, month, 1) for month in range(start, end)]
def get_free_paid_breakdown_for_billable_units(year, billable_units):
cumulative = 0
for month in get_months_for_financial_year(year):
previous_cumulative = cumulative
monthly_usage = billable_units.get(month, 0)
cumulative += monthly_usage
breakdown = get_free_paid_breakdown_for_month(
cumulative, previous_cumulative, monthly_usage
)
yield {
'name': month,
'paid': breakdown['paid'],
'free': breakdown['free']
}
def get_free_paid_breakdown_for_month(
cumulative,
previous_cumulative,
monthly_usage
):
allowance = 250000
if cumulative < allowance:
return {
'paid': 0,
'free': monthly_usage,
}
elif previous_cumulative < allowance:
return {
'paid': monthly_usage - (allowance - previous_cumulative),
'free': allowance - previous_cumulative
}
else:
return {
'paid': monthly_usage,
'free': 0
}