2016-01-06 11:03:29 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2016-08-05 10:18:27 +01:00
|
|
|
import ago
|
2015-12-18 10:26:56 +00:00
|
|
|
import time
|
2016-07-18 09:05:00 +01:00
|
|
|
import dateutil
|
2016-08-05 10:59:36 +01:00
|
|
|
from orderedset import OrderedSet
|
2016-07-18 09:05:00 +01:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2016-08-05 10:18:27 +01:00
|
|
|
from itertools import chain
|
2016-01-13 17:32:40 +00:00
|
|
|
|
|
|
|
|
from flask import (
|
|
|
|
|
render_template,
|
2016-03-02 17:36:20 +00:00
|
|
|
abort,
|
2016-03-16 16:57:10 +00:00
|
|
|
jsonify,
|
2016-04-13 12:50:28 +01:00
|
|
|
request,
|
2016-04-28 15:50:05 +01:00
|
|
|
url_for,
|
|
|
|
|
current_app
|
2016-01-13 17:32:40 +00:00
|
|
|
)
|
2016-01-11 16:03:41 +00:00
|
|
|
from flask_login import login_required
|
2016-04-04 16:34:06 +01:00
|
|
|
from werkzeug.datastructures import MultiDict
|
2016-04-14 12:00:55 +01:00
|
|
|
from notifications_utils.template import Template
|
2016-01-11 16:03:41 +00:00
|
|
|
|
2016-04-04 16:53:52 +01:00
|
|
|
from app import (
|
|
|
|
|
job_api_client,
|
|
|
|
|
notification_api_client,
|
|
|
|
|
service_api_client,
|
2016-05-24 09:52:43 +01:00
|
|
|
current_service,
|
|
|
|
|
format_datetime_short)
|
2015-12-17 11:14:19 +00:00
|
|
|
from app.main import main
|
2016-03-29 22:50:40 +01:00
|
|
|
from app.utils import (
|
|
|
|
|
get_page_from_request,
|
|
|
|
|
generate_previous_next_dict,
|
2016-04-12 14:19:51 +01:00
|
|
|
user_has_permissions,
|
|
|
|
|
generate_notifications_csv)
|
2016-07-20 15:19:58 +01:00
|
|
|
from app.statistics_utils import add_rate_to_jobs
|
2016-07-05 11:39:07 +01:00
|
|
|
from app.utils import get_help_argument
|
2015-12-18 10:26:56 +00:00
|
|
|
|
2015-12-17 11:14:19 +00:00
|
|
|
|
2016-04-04 16:34:06 +01:00
|
|
|
def _parse_filter_args(filter_dict):
|
|
|
|
|
if not isinstance(filter_dict, MultiDict):
|
|
|
|
|
filter_dict = MultiDict(filter_dict)
|
2016-04-13 12:47:24 +01:00
|
|
|
|
|
|
|
|
return MultiDict(
|
|
|
|
|
(
|
|
|
|
|
key,
|
|
|
|
|
(','.join(filter_dict.getlist(key))).split(',')
|
|
|
|
|
)
|
|
|
|
|
for key in filter_dict.keys()
|
|
|
|
|
if ''.join(filter_dict.getlist(key))
|
|
|
|
|
)
|
2016-04-04 16:34:06 +01:00
|
|
|
|
|
|
|
|
|
2016-05-18 11:44:58 +01:00
|
|
|
def _set_status_filters(filter_args):
|
2016-08-05 10:18:27 +01:00
|
|
|
status_filters = filter_args.get('status', [])
|
Add filters for ‘processed’ and sending states
- _Processed_ is all the notifications that we know about, ie sending,
failed and delivered
- _Sending_ is notifications that we have either put into a queue or are
waiting to hear back from the provider about.
The big numbers on the dashboard are a count of all the messages we’ve
processed. So when you click them, the table of notifications you see
on the dashboard should contain that number of notifications.
This also gets the activity page one step closer to being like the job
page:
| Before | After
---------|----------------------------|---------------------------------
Activity | Sending, failed, both | Processed, sending, failed, delivered
Job page | Sending, failed, delivered | Sending, failed, delivered
2016-06-07 12:01:43 +01:00
|
|
|
all_failure_statuses = ['failed', 'temporary-failure', 'permanent-failure', 'technical-failure']
|
2016-08-03 09:47:12 +01:00
|
|
|
all_sending_statuses = ['created', 'sending']
|
2016-08-05 10:59:36 +01:00
|
|
|
return list(OrderedSet(chain(
|
|
|
|
|
(status_filters or all_sending_statuses + ['delivered'] + all_failure_statuses),
|
|
|
|
|
all_sending_statuses if 'sending' in status_filters else [],
|
|
|
|
|
all_failure_statuses if 'failed' in status_filters else []
|
|
|
|
|
)))
|
2016-05-18 11:44:58 +01:00
|
|
|
|
|
|
|
|
|
2016-02-02 14:24:08 +00:00
|
|
|
@main.route("/services/<service_id>/jobs")
|
2016-01-11 16:03:41 +00:00
|
|
|
@login_required
|
2016-03-29 13:23:36 +01:00
|
|
|
@user_has_permissions('view_activity', admin_override=True)
|
2016-01-15 17:46:09 +00:00
|
|
|
def view_jobs(service_id):
|
2016-03-10 11:53:29 +00:00
|
|
|
return render_template(
|
|
|
|
|
'views/jobs/jobs.html',
|
2016-06-16 11:06:57 +01:00
|
|
|
jobs=add_rate_to_jobs(job_api_client.get_job(service_id)['data'])
|
2016-03-10 11:53:29 +00:00
|
|
|
)
|
2015-12-17 11:14:19 +00:00
|
|
|
|
|
|
|
|
|
2016-02-02 14:24:08 +00:00
|
|
|
@main.route("/services/<service_id>/jobs/<job_id>")
|
2016-01-11 16:03:41 +00:00
|
|
|
@login_required
|
2016-03-29 13:23:36 +01:00
|
|
|
@user_has_permissions('view_activity', admin_override=True)
|
2016-01-15 17:46:09 +00:00
|
|
|
def view_job(service_id, job_id):
|
2016-06-08 16:34:26 +01:00
|
|
|
|
2016-06-28 11:23:43 +01:00
|
|
|
job = job_api_client.get_job(service_id, job_id)['data']
|
2016-06-08 16:34:26 +01:00
|
|
|
filter_args = _parse_filter_args(request.args)
|
2016-08-05 10:18:27 +01:00
|
|
|
filter_args['status'] = _set_status_filters(filter_args)
|
2016-06-28 11:23:43 +01:00
|
|
|
|
2016-08-25 09:15:55 +01:00
|
|
|
total_notifications = job.get('notification_count', 0)
|
|
|
|
|
processed_notifications = job.get('notifications_delivered', 0) + job.get('notifications_failed', 0)
|
|
|
|
|
|
2016-03-10 11:53:29 +00:00
|
|
|
return render_template(
|
|
|
|
|
'views/jobs/job.html',
|
2016-08-25 09:15:55 +01:00
|
|
|
finished=(total_notifications == processed_notifications),
|
2016-03-10 11:53:29 +00:00
|
|
|
uploaded_file_name=job['original_file_name'],
|
|
|
|
|
template=Template(
|
2016-06-28 11:23:43 +01:00
|
|
|
service_api_client.get_service_template(
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
template_id=job['template'],
|
|
|
|
|
version=job['template_version']
|
|
|
|
|
)['data'],
|
2016-04-04 16:53:52 +01:00
|
|
|
prefix=current_service['name']
|
2016-06-08 16:34:26 +01:00
|
|
|
),
|
2016-06-28 09:21:46 +01:00
|
|
|
status=request.args.get('status', ''),
|
|
|
|
|
updates_url=url_for(
|
|
|
|
|
".view_job_updates",
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
job_id=job['id'],
|
2016-07-19 09:09:20 +01:00
|
|
|
status=request.args.get('status', ''),
|
|
|
|
|
help=get_help_argument()
|
2016-06-28 11:23:43 +01:00
|
|
|
),
|
2016-07-05 11:39:07 +01:00
|
|
|
partials=get_job_partials(job),
|
|
|
|
|
help=get_help_argument()
|
2016-03-10 11:53:29 +00:00
|
|
|
)
|
2015-12-17 11:14:19 +00:00
|
|
|
|
|
|
|
|
|
2016-06-07 11:41:20 +01:00
|
|
|
@main.route("/services/<service_id>/jobs/<job_id>.csv")
|
|
|
|
|
@login_required
|
|
|
|
|
@user_has_permissions('view_activity', admin_override=True)
|
|
|
|
|
def view_job_csv(service_id, job_id):
|
|
|
|
|
job = job_api_client.get_job(service_id, job_id)['data']
|
|
|
|
|
template = service_api_client.get_service_template(
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
template_id=job['template'],
|
|
|
|
|
version=job['template_version']
|
|
|
|
|
)['data']
|
2016-06-08 16:34:26 +01:00
|
|
|
filter_args = _parse_filter_args(request.args)
|
2016-08-05 10:18:27 +01:00
|
|
|
filter_args['status'] = _set_status_filters(filter_args)
|
2016-06-07 11:41:20 +01:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
generate_notifications_csv(
|
|
|
|
|
notification_api_client.get_notifications_for_service(
|
|
|
|
|
service_id,
|
|
|
|
|
job_id,
|
2016-06-08 16:34:26 +01:00
|
|
|
status=filter_args.get('status'),
|
2016-06-07 11:41:20 +01:00
|
|
|
page_size=job['notification_count']
|
|
|
|
|
)['notifications']
|
|
|
|
|
),
|
|
|
|
|
200,
|
|
|
|
|
{
|
|
|
|
|
'Content-Type': 'text/csv; charset=utf-8',
|
|
|
|
|
'Content-Disposition': 'inline; filename="{} - {}.csv"'.format(
|
|
|
|
|
template['name'],
|
|
|
|
|
format_datetime_short(job['created_at'])
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2016-03-02 17:36:20 +00:00
|
|
|
@main.route("/services/<service_id>/jobs/<job_id>.json")
|
|
|
|
|
@login_required
|
2016-05-27 19:13:01 +01:00
|
|
|
@user_has_permissions('view_activity', admin_override=True)
|
2016-03-02 17:36:20 +00:00
|
|
|
def view_job_updates(service_id, job_id):
|
2016-06-28 11:23:43 +01:00
|
|
|
return jsonify(**get_job_partials(
|
|
|
|
|
job_api_client.get_job(service_id, job_id)['data']
|
|
|
|
|
))
|
2016-03-02 17:36:20 +00:00
|
|
|
|
|
|
|
|
|
2016-06-07 11:17:33 +01:00
|
|
|
@main.route('/services/<service_id>/notifications/<message_type>')
|
2016-06-07 11:41:20 +01:00
|
|
|
@main.route('/services/<service_id>/notifications/<message_type>.csv', endpoint="view_notifications_csv")
|
2016-03-16 16:57:10 +00:00
|
|
|
@login_required
|
2016-03-29 13:23:36 +01:00
|
|
|
@user_has_permissions('view_activity', admin_override=True)
|
2016-06-07 11:17:33 +01:00
|
|
|
def view_notifications(service_id, message_type):
|
2016-03-16 16:57:10 +00:00
|
|
|
# TODO get the api to return count of pages as well.
|
|
|
|
|
page = get_page_from_request()
|
|
|
|
|
if page is None:
|
|
|
|
|
abort(404, "Invalid page argument ({}) reverting to page 1.".format(request.args['page'], None))
|
2016-06-07 11:17:33 +01:00
|
|
|
if message_type not in ['email', 'sms']:
|
|
|
|
|
abort(404)
|
2016-04-28 15:50:05 +01:00
|
|
|
|
2016-04-04 16:34:06 +01:00
|
|
|
filter_args = _parse_filter_args(request.args)
|
2016-08-05 10:18:27 +01:00
|
|
|
filter_args['status'] = _set_status_filters(filter_args)
|
2016-04-28 15:50:05 +01:00
|
|
|
|
2016-04-04 16:34:06 +01:00
|
|
|
notifications = notification_api_client.get_notifications_for_service(
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
page=page,
|
2016-06-07 11:17:33 +01:00
|
|
|
template_type=[message_type],
|
2016-05-18 11:44:58 +01:00
|
|
|
status=filter_args.get('status'),
|
2016-04-28 15:50:05 +01:00
|
|
|
limit_days=current_app.config['ACTIVITY_STATS_LIMIT_DAYS'])
|
2016-06-07 11:17:33 +01:00
|
|
|
view_dict = dict(
|
|
|
|
|
message_type=message_type,
|
2016-08-05 11:33:32 +01:00
|
|
|
status=request.args.get('status')
|
2016-06-07 11:17:33 +01:00
|
|
|
)
|
2016-03-16 16:57:10 +00:00
|
|
|
prev_page = None
|
|
|
|
|
if notifications['links'].get('prev', None):
|
|
|
|
|
prev_page = generate_previous_next_dict(
|
|
|
|
|
'main.view_notifications',
|
2016-04-04 16:34:06 +01:00
|
|
|
service_id,
|
|
|
|
|
view_dict,
|
|
|
|
|
page - 1,
|
|
|
|
|
'Previous page',
|
|
|
|
|
'page {}'.format(page - 1))
|
2016-03-16 16:57:10 +00:00
|
|
|
next_page = None
|
|
|
|
|
if notifications['links'].get('next', None):
|
|
|
|
|
next_page = generate_previous_next_dict(
|
|
|
|
|
'main.view_notifications',
|
2016-04-04 16:34:06 +01:00
|
|
|
service_id,
|
|
|
|
|
view_dict,
|
|
|
|
|
page + 1,
|
|
|
|
|
'Next page',
|
|
|
|
|
'page {}'.format(page + 1))
|
2016-06-07 11:41:20 +01:00
|
|
|
if request.path.endswith('csv'):
|
2016-04-19 11:45:36 +01:00
|
|
|
csv_content = generate_notifications_csv(
|
|
|
|
|
notification_api_client.get_notifications_for_service(
|
|
|
|
|
service_id=service_id,
|
|
|
|
|
page=page,
|
|
|
|
|
page_size=notifications['total'],
|
2016-06-07 11:17:33 +01:00
|
|
|
template_type=[message_type],
|
2016-05-18 11:44:58 +01:00
|
|
|
status=filter_args.get('status'),
|
2016-04-28 15:50:05 +01:00
|
|
|
limit_days=current_app.config['ACTIVITY_STATS_LIMIT_DAYS'])['notifications'])
|
2016-04-12 14:19:51 +01:00
|
|
|
return csv_content, 200, {
|
|
|
|
|
'Content-Type': 'text/csv; charset=utf-8',
|
|
|
|
|
'Content-Disposition': 'inline; filename="notifications.csv"'
|
|
|
|
|
}
|
2016-03-16 16:57:10 +00:00
|
|
|
return render_template(
|
|
|
|
|
'views/notifications.html',
|
|
|
|
|
notifications=notifications['notifications'],
|
|
|
|
|
page=page,
|
|
|
|
|
prev_page=prev_page,
|
2016-04-13 12:47:24 +01:00
|
|
|
next_page=next_page,
|
2016-08-05 10:18:27 +01:00
|
|
|
status=request.args.get('status'),
|
2016-06-07 11:17:33 +01:00
|
|
|
message_type=message_type,
|
2016-06-07 11:41:20 +01:00
|
|
|
download_link=url_for(
|
|
|
|
|
'.view_notifications_csv',
|
|
|
|
|
service_id=current_service['id'],
|
|
|
|
|
message_type=message_type,
|
|
|
|
|
status=request.args.get('status')
|
|
|
|
|
),
|
2016-07-20 15:19:58 +01:00
|
|
|
status_filters=get_status_filters(
|
|
|
|
|
current_service,
|
|
|
|
|
message_type,
|
|
|
|
|
service_api_client.get_detailed_service(service_id)['data']['statistics']
|
|
|
|
|
)
|
2016-03-16 16:57:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2016-02-02 14:24:08 +00:00
|
|
|
@main.route("/services/<service_id>/jobs/<job_id>/notification/<string:notification_id>")
|
2016-01-11 16:03:41 +00:00
|
|
|
@login_required
|
2016-03-29 13:23:36 +01:00
|
|
|
@user_has_permissions('view_activity', admin_override=True)
|
2016-01-15 17:46:09 +00:00
|
|
|
def view_notification(service_id, job_id, notification_id):
|
2016-03-02 17:36:20 +00:00
|
|
|
|
|
|
|
|
now = time.strftime('%H:%M')
|
|
|
|
|
|
2015-12-17 11:14:19 +00:00
|
|
|
return render_template(
|
|
|
|
|
'views/notification.html',
|
|
|
|
|
message=[
|
|
|
|
|
message for message in messages if message['id'] == notification_id
|
2015-12-17 14:58:37 +00:00
|
|
|
][0],
|
2015-12-18 17:03:49 +00:00
|
|
|
delivered_at=now,
|
2016-01-13 12:10:29 +00:00
|
|
|
uploaded_at=now,
|
2016-01-13 12:49:31 +00:00
|
|
|
job_id=job_id
|
2015-12-17 11:14:19 +00:00
|
|
|
)
|
2016-06-08 16:34:26 +01:00
|
|
|
|
|
|
|
|
|
2016-07-20 15:19:58 +01:00
|
|
|
def get_status_filters(service, message_type, statistics):
|
|
|
|
|
stats = statistics[message_type]
|
|
|
|
|
stats['sending'] = stats['requested'] - stats['delivered'] - stats['failed']
|
|
|
|
|
|
|
|
|
|
filters = [
|
|
|
|
|
# key, label, option
|
2016-08-03 09:47:27 +01:00
|
|
|
('requested', 'total', 'sending,delivered,failed'),
|
2016-07-20 15:19:58 +01:00
|
|
|
('sending', 'sending', 'sending'),
|
|
|
|
|
('delivered', 'delivered', 'delivered'),
|
|
|
|
|
('failed', 'failed', 'failed'),
|
|
|
|
|
]
|
|
|
|
|
return [
|
|
|
|
|
# return list containing label, option, link, count
|
|
|
|
|
(
|
|
|
|
|
label,
|
|
|
|
|
option,
|
|
|
|
|
url_for(
|
|
|
|
|
'.view_notifications',
|
|
|
|
|
service_id=service['id'],
|
|
|
|
|
message_type=message_type,
|
|
|
|
|
status=option
|
|
|
|
|
),
|
|
|
|
|
stats[key]
|
|
|
|
|
)
|
|
|
|
|
for key, label, option in filters
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2016-06-08 16:34:26 +01:00
|
|
|
def _get_job_counts(job, help_argument):
|
|
|
|
|
return [
|
|
|
|
|
(
|
|
|
|
|
label,
|
|
|
|
|
query_param,
|
|
|
|
|
url_for(
|
|
|
|
|
".view_job",
|
|
|
|
|
service_id=job['service'],
|
|
|
|
|
job_id=job['id'],
|
|
|
|
|
status=query_param,
|
|
|
|
|
help=help_argument
|
|
|
|
|
),
|
|
|
|
|
count
|
|
|
|
|
) for label, query_param, count in [
|
|
|
|
|
[
|
2016-08-03 09:47:27 +01:00
|
|
|
'total', '',
|
|
|
|
|
job.get('notification_count', 0)
|
2016-06-08 16:34:26 +01:00
|
|
|
],
|
|
|
|
|
[
|
2016-08-03 09:47:12 +01:00
|
|
|
'sending', 'sending',
|
|
|
|
|
job.get('notification_count', 0) -
|
|
|
|
|
job.get('notifications_delivered', 0) -
|
|
|
|
|
job.get('notifications_failed', 0)
|
2016-06-08 16:34:26 +01:00
|
|
|
],
|
|
|
|
|
[
|
2016-08-03 09:47:27 +01:00
|
|
|
'delivered', 'delivered',
|
2016-06-08 16:34:26 +01:00
|
|
|
job.get('notifications_delivered', 0)
|
|
|
|
|
],
|
|
|
|
|
[
|
2016-08-03 09:47:27 +01:00
|
|
|
'failed', 'failed',
|
|
|
|
|
job.get('notifications_failed', 0)
|
2016-06-08 16:34:26 +01:00
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
]
|
2016-06-28 11:23:43 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_job_partials(job):
|
|
|
|
|
filter_args = _parse_filter_args(request.args)
|
2016-08-05 10:18:27 +01:00
|
|
|
filter_args['status'] = _set_status_filters(filter_args)
|
2016-08-02 21:27:23 +01:00
|
|
|
notifications = notification_api_client.get_notifications_for_service(
|
2016-08-05 10:18:27 +01:00
|
|
|
job['service'], job['id'], status=filter_args['status']
|
2016-08-02 21:27:23 +01:00
|
|
|
)
|
2016-06-28 11:23:43 +01:00
|
|
|
return {
|
|
|
|
|
'counts': render_template(
|
|
|
|
|
'partials/jobs/count.html',
|
|
|
|
|
job=job,
|
|
|
|
|
counts=_get_job_counts(job, request.args.get('help', 0)),
|
2016-08-05 10:18:27 +01:00
|
|
|
status=filter_args['status']
|
2016-06-28 11:23:43 +01:00
|
|
|
),
|
|
|
|
|
'notifications': render_template(
|
|
|
|
|
'partials/jobs/notifications.html',
|
2016-08-02 21:27:23 +01:00
|
|
|
notifications=notifications['notifications'],
|
|
|
|
|
more_than_one_page=bool(notifications.get('links', {}).get('next')),
|
|
|
|
|
percentage_complete=(job['notifications_sent'] / job['notification_count'] * 100),
|
2016-07-18 09:04:43 +01:00
|
|
|
download_link=url_for(
|
|
|
|
|
'.view_job_csv',
|
|
|
|
|
service_id=current_service['id'],
|
|
|
|
|
job_id=job['id'],
|
2016-08-05 10:18:27 +01:00
|
|
|
status=request.args.get('status')
|
2016-07-18 09:04:43 +01:00
|
|
|
),
|
2016-07-18 09:05:00 +01:00
|
|
|
help=get_help_argument(),
|
2016-07-20 11:50:22 +01:00
|
|
|
time_left=get_time_left(job['created_at'])
|
2016-06-28 11:23:43 +01:00
|
|
|
),
|
|
|
|
|
'status': render_template(
|
|
|
|
|
'partials/jobs/status.html',
|
|
|
|
|
job=job
|
|
|
|
|
),
|
|
|
|
|
}
|
2016-07-20 11:50:22 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_time_left(job_created_at):
|
|
|
|
|
return ago.human(
|
|
|
|
|
(
|
|
|
|
|
datetime.now(timezone.utc).replace(hour=23, minute=59, second=59)
|
|
|
|
|
) - (
|
|
|
|
|
dateutil.parser.parse(job_created_at) + timedelta(days=8)
|
|
|
|
|
),
|
|
|
|
|
future_tense='Data available for {}',
|
|
|
|
|
past_tense='Data no longer available', # No-one should ever see this
|
|
|
|
|
precision=1
|
|
|
|
|
)
|