Put the download csv link on the activity page.

Still needs some tests, probably some refactoring as well.
This commit is contained in:
Rebecca Law
2017-12-30 16:54:39 +00:00
parent 31b3147f1d
commit 259db9c271
5 changed files with 116 additions and 64 deletions

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from orderedset import OrderedSet
from itertools import chain
from flask import ( from flask import (
render_template, render_template,
@@ -18,7 +16,6 @@ from notifications_utils.template import (
Template, Template,
WithSubjectTemplate, WithSubjectTemplate,
) )
from werkzeug.datastructures import MultiDict
from app import ( from app import (
job_api_client, job_api_client,
@@ -35,39 +32,11 @@ from app.utils import (
user_has_permissions, user_has_permissions,
generate_notifications_csv, generate_notifications_csv,
get_time_left, get_time_left,
REQUESTED_STATUSES,
FAILURE_STATUSES,
SENDING_STATUSES,
DELIVERED_STATUSES,
get_letter_timings, get_letter_timings,
) parse_filter_args, set_status_filters)
from app.statistics_utils import add_rate_to_job from app.statistics_utils import add_rate_to_job
def _parse_filter_args(filter_dict):
if not isinstance(filter_dict, MultiDict):
filter_dict = MultiDict(filter_dict)
return MultiDict(
(
key,
(','.join(filter_dict.getlist(key))).split(',')
)
for key in filter_dict.keys()
if ''.join(filter_dict.getlist(key))
)
def _set_status_filters(filter_args):
status_filters = filter_args.get('status', [])
return list(OrderedSet(chain(
(status_filters or REQUESTED_STATUSES),
DELIVERED_STATUSES if 'delivered' in status_filters else [],
SENDING_STATUSES if 'sending' in status_filters else [],
FAILURE_STATUSES if 'failed' in status_filters else []
)))
@main.route("/services/<service_id>/jobs") @main.route("/services/<service_id>/jobs")
@login_required @login_required
@user_has_permissions('view_activity', admin_override=True) @user_has_permissions('view_activity', admin_override=True)
@@ -104,8 +73,8 @@ def view_job(service_id, job_id):
if job['job_status'] == 'cancelled': if job['job_status'] == 'cancelled':
abort(404) abort(404)
filter_args = _parse_filter_args(request.args) filter_args = parse_filter_args(request.args)
filter_args['status'] = _set_status_filters(filter_args) filter_args['status'] = set_status_filters(filter_args)
total_notifications = job.get('notification_count', 0) total_notifications = job.get('notification_count', 0)
processed_notifications = job.get('notifications_delivered', 0) + job.get('notifications_failed', 0) processed_notifications = job.get('notifications_delivered', 0) + job.get('notifications_failed', 0)
@@ -146,8 +115,8 @@ def view_job_csv(service_id, job_id):
template_id=job['template'], template_id=job['template'],
version=job['template_version'] version=job['template_version']
)['data'] )['data']
filter_args = _parse_filter_args(request.args) filter_args = parse_filter_args(request.args)
filter_args['status'] = _set_status_filters(filter_args) filter_args['status'] = set_status_filters(filter_args)
return Response( return Response(
stream_with_context( stream_with_context(
@@ -206,6 +175,12 @@ def view_notifications(service_id, message_type):
page=request.args.get('page', 1), page=request.args.get('page', 1),
to=request.form.get('to', ''), to=request.form.get('to', ''),
search_form=SearchNotificationsForm(to=request.form.get('to', '')), search_form=SearchNotificationsForm(to=request.form.get('to', '')),
download_link=url_for(
'.download_notifications_csv',
service_id=current_service['id'],
message_type=message_type,
status=request.args.get('status')
)
) )
@@ -226,8 +201,8 @@ def get_notifications(service_id, message_type, status_override=None):
abort(404, "Invalid page argument ({}) reverting to page 1.".format(request.args['page'], None)) abort(404, "Invalid page argument ({}) reverting to page 1.".format(request.args['page'], None))
if message_type not in ['email', 'sms', 'letter']: if message_type not in ['email', 'sms', 'letter']:
abort(404) abort(404)
filter_args = _parse_filter_args(request.args) filter_args = parse_filter_args(request.args)
filter_args['status'] = _set_status_filters(filter_args) filter_args['status'] = set_status_filters(filter_args)
if request.path.endswith('csv'): if request.path.endswith('csv'):
return Response( return Response(
generate_notifications_csv( generate_notifications_csv(
@@ -250,7 +225,6 @@ def get_notifications(service_id, message_type, status_override=None):
limit_days=current_app.config['ACTIVITY_STATS_LIMIT_DAYS'], limit_days=current_app.config['ACTIVITY_STATS_LIMIT_DAYS'],
to=request.form.get('to', ''), to=request.form.get('to', ''),
) )
url_args = { url_args = {
'message_type': message_type, 'message_type': message_type,
'status': request.args.get('status') 'status': request.args.get('status')
@@ -361,8 +335,8 @@ def _get_job_counts(job):
def get_job_partials(job, template): def get_job_partials(job, template):
filter_args = _parse_filter_args(request.args) filter_args = parse_filter_args(request.args)
filter_args['status'] = _set_status_filters(filter_args) filter_args['status'] = set_status_filters(filter_args)
notifications = notification_api_client.get_notifications_for_service( notifications = notification_api_client.get_notifications_for_service(
job['service'], job['id'], status=filter_args['status'] job['service'], job['id'], status=filter_args['status']
) )

View File

@@ -1,18 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime
from flask import ( from flask import (
abort, abort,
render_template, render_template,
jsonify, jsonify,
request, request,
url_for, url_for,
) Response, stream_with_context)
from flask_login import login_required from flask_login import login_required
from app import ( from app import (
notification_api_client, notification_api_client,
job_api_client, job_api_client,
current_service current_service,
) format_date_numeric)
from app.main import main from app.main import main
from app.template_previews import TemplatePreview from app.template_previews import TemplatePreview
from app.utils import ( from app.utils import (
@@ -23,7 +25,7 @@ from app.utils import (
get_letter_timings, get_letter_timings,
FAILURE_STATUSES, FAILURE_STATUSES,
DELIVERED_STATUSES, DELIVERED_STATUSES,
) generate_notifications_csv, parse_filter_args, set_status_filters)
@main.route("/services/<service_id>/notification/<uuid:notification_id>") @main.route("/services/<service_id>/notification/<uuid:notification_id>")
@@ -134,3 +136,31 @@ def get_all_personalisation_from_notification(notification):
notification['personalisation']['phone_number'] = notification['to'] notification['personalisation']['phone_number'] = notification['to']
return notification['personalisation'] return notification['personalisation']
@main.route("/services/<service_id>/download-notifications.csv")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def download_notifications_csv(service_id):
filter_args = parse_filter_args(request.args)
filter_args['status'] = set_status_filters(filter_args)
return Response(
stream_with_context(
generate_notifications_csv(
service_id=service_id,
job_id=None,
status=filter_args.get('status'),
page=request.args.get('page', 1),
page_size=5000,
format_for_csv=True
)
),
mimetype='text/csv',
headers={
'Content-Disposition': 'inline; filename="{} - {} - {} report.csv"'.format(
format_date_numeric(datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ")),
filter_args['message_type'][0],
current_service['name'])
}
)

View File

@@ -43,7 +43,11 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
</form> </form>
{% endif %} {% endif %}
<p class="bottom-gutter">
<a href="{{ download_link }}" download="download" class="heading-small">Download this report</a>
&emsp;
Data available for 7 days
</p>
{{ ajax_block( {{ ajax_block(
partials, partials,
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page), url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page),

View File

@@ -1,5 +1,7 @@
import re import re
import csv import csv
from itertools import chain
import pytz import pytz
from io import StringIO from io import StringIO
from os import path from os import path
@@ -28,7 +30,8 @@ from notifications_utils.template import (
LetterImageTemplate, LetterImageTemplate,
LetterPreviewTemplate, LetterPreviewTemplate,
) )
from orderedset._orderedset import OrderedSet
from werkzeug.datastructures import MultiDict
SENDING_STATUSES = ['created', 'pending', 'sending'] SENDING_STATUSES = ['created', 'pending', 'sending']
DELIVERED_STATUSES = ['delivered', 'sent'] DELIVERED_STATUSES = ['delivered', 'sent']
@@ -132,21 +135,36 @@ def generate_notifications_csv(**kwargs):
yield ','.join(fieldnames) + '\n' yield ','.join(fieldnames) + '\n'
while kwargs['page']: while kwargs['page']:
# if job_id then response looks different
notifications_resp = notification_api_client.get_notifications_for_service(**kwargs) notifications_resp = notification_api_client.get_notifications_for_service(**kwargs)
notifications = notifications_resp['notifications'] notifications = notifications_resp['notifications']
for notification in notifications: if kwargs['job_id']:
values = [ for notification in notifications:
notification['row_number'], values = [
notification['recipient'], notification['row_number'],
notification['template_name'], notification['recipient'],
notification['template_type'], notification['template_name'],
notification['job_name'], notification['template_type'],
notification['status'], notification['job_name'],
notification['created_at'] notification['status'],
] notification['created_at']
line = ','.join(str(i) for i in values) + '\n' ]
yield line line = ','.join(str(i) for i in values) + '\n'
yield line
else:
# Change here
for notification in notifications:
values = [
notification.get('row_number', None),
notification['to'],
notification['template']['name'],
notification['template']['template_type'],
notification.get('job_name', None),
notification['status'],
notification['created_at']
]
line = ','.join(str(i) for i in values) + '\n'
yield line
if notifications_resp['links'].get('next'): if notifications_resp['links'].get('next'):
kwargs['page'] += 1 kwargs['page'] += 1
else: else:
@@ -381,3 +399,27 @@ def get_cdn_domain():
domain = parsed_uri.netloc[len(subdomain + '.'):] domain = parsed_uri.netloc[len(subdomain + '.'):]
return "static-logos.{}".format(domain) return "static-logos.{}".format(domain)
def parse_filter_args(filter_dict):
if not isinstance(filter_dict, MultiDict):
filter_dict = MultiDict(filter_dict)
return MultiDict(
(
key,
(','.join(filter_dict.getlist(key))).split(',')
)
for key in filter_dict.keys()
if ''.join(filter_dict.getlist(key))
)
def set_status_filters(filter_args):
status_filters = filter_args.get('status', [])
return list(OrderedSet(chain(
(status_filters or REQUESTED_STATUSES),
DELIVERED_STATUSES if 'delivered' in status_filters else [],
SENDING_STATUSES if 'sending' in status_filters else [],
FAILURE_STATUSES if 'failed' in status_filters else []
)))

View File

@@ -1,3 +1,4 @@
import uuid
from pathlib import Path from pathlib import Path
from io import StringIO from io import StringIO
from collections import OrderedDict from collections import OrderedDict
@@ -58,7 +59,8 @@ def _get_notifications_csv(
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def _get_notifications_csv_mock( def _get_notifications_csv_mock(
mocker, mocker,
api_user_active api_user_active,
job_id=None
): ):
return mocker.patch( return mocker.patch(
'app.notification_api_client.get_notifications_for_service', 'app.notification_api_client.get_notifications_for_service',
@@ -122,13 +124,13 @@ def test_can_create_spreadsheet_from_dict_with_filename():
def test_generate_notifications_csv_returns_correct_csv_file(_get_notifications_csv_mock): def test_generate_notifications_csv_returns_correct_csv_file(_get_notifications_csv_mock):
csv_content = generate_notifications_csv(service_id='1234') csv_content = generate_notifications_csv(service_id='1234', job_id=uuid.uuid4())
csv_file = DictReader(StringIO('\n'.join(csv_content))) csv_file = DictReader(StringIO('\n'.join(csv_content)))
assert csv_file.fieldnames == ['Row number', 'Recipient', 'Template', 'Type', 'Job', 'Status', 'Time'] assert csv_file.fieldnames == ['Row number', 'Recipient', 'Template', 'Type', 'Job', 'Status', 'Time']
def test_generate_notifications_csv_only_calls_once_if_no_next_link(_get_notifications_csv_mock): def test_generate_notifications_csv_only_calls_once_if_no_next_link(_get_notifications_csv_mock):
list(generate_notifications_csv(service_id='1234')) list(generate_notifications_csv(service_id='1234', job_id=uuid.uuid4()))
assert _get_notifications_csv_mock.call_count == 1 assert _get_notifications_csv_mock.call_count == 1
@@ -146,7 +148,7 @@ def test_generate_notifications_csv_calls_twice_if_next_link(mocker):
] ]
) )
csv_content = generate_notifications_csv(service_id=service_id) csv_content = generate_notifications_csv(service_id=service_id, job_id=uuid.uuid4())
csv = DictReader(StringIO('\n'.join(csv_content))) csv = DictReader(StringIO('\n'.join(csv_content)))
assert len(list(csv)) == 10 assert len(list(csv)) == 10