From c5f92eabfb62b04cd2029b307dee9458b3fe8077 Mon Sep 17 00:00:00 2001 From: Leo Hemsted Date: Mon, 12 Jun 2017 17:21:25 +0100 Subject: [PATCH] add add one-off notification status completely mimicks the job status page, and as such, all the code and templates have been taken from the job page. This page performs exactly the same as the job page for now * total, sending, delivered, failed blue boxes (though they'll just read 0/1 for now. * download report button (same as with job download, except without job or row number in file) * removed references to scheduled * kept references to help (aka tour/tutorial) as that'll eventually change over from a job to a one-off too --- app/main/__init__.py | 1 + app/main/views/jobs.py | 19 +-- app/main/views/notifications.py | 137 ++++++++++++++++++ app/notify_client/notification_api_client.py | 6 +- app/templates/partials/{jobs => }/count.html | 0 .../partials/notifications/notifications.html | 39 +++++ .../partials/notifications/status.html | 6 + .../views/notifications/notification.html | 27 ++++ app/utils.py | 50 +++++-- 9 files changed, 255 insertions(+), 30 deletions(-) create mode 100644 app/main/views/notifications.py rename app/templates/partials/{jobs => }/count.html (100%) create mode 100644 app/templates/partials/notifications/notifications.html create mode 100644 app/templates/partials/notifications/status.html create mode 100644 app/templates/views/notifications/notification.html diff --git a/app/main/__init__.py b/app/main/__init__.py index c6b2161a9..506d07858 100644 --- a/app/main/__init__.py +++ b/app/main/__init__.py @@ -29,4 +29,5 @@ from app.main.views import ( platform_admin, letter_jobs, conversation, + notifications ) diff --git a/app/main/views/jobs.py b/app/main/views/jobs.py index d10028b35..266811651 100644 --- a/app/main/views/jobs.py +++ b/app/main/views/jobs.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -import ago -import dateutil from orderedset import OrderedSet -from datetime import datetime, timedelta, timezone from itertools import chain from flask import ( @@ -35,6 +32,7 @@ from app.utils import ( generate_notifications_csv, get_help_argument, get_template, + get_time_left, REQUESTED_STATUSES, FAILURE_STATUSES, SENDING_STATUSES, @@ -364,7 +362,7 @@ def get_job_partials(job): ) return { 'counts': render_template( - 'partials/jobs/count.html', + 'partials/count.html', counts=_get_job_counts(job, request.args.get('help', 0)), status=filter_args['status'] ), @@ -388,16 +386,3 @@ def get_job_partials(job): job=job ), } - - -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 - ) diff --git a/app/main/views/notifications.py b/app/main/views/notifications.py new file mode 100644 index 000000000..ec51789bd --- /dev/null +++ b/app/main/views/notifications.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +from flask import ( + render_template, + jsonify, + request, + url_for, + current_app +) +from flask_login import login_required + +from app import ( + notification_api_client, + current_service +) +from app.main import main +from app.utils import ( + user_has_permissions, + get_help_argument, + get_template, + get_time_left, + REQUESTED_STATUSES, + FAILURE_STATUSES, + SENDING_STATUSES, + DELIVERED_STATUSES, +) + + +def get_status_arg(filter_args): + if 'status' not in filter_args or not filter_args['status']: + return REQUESTED_STATUSES + elif filter_args['status'] == 'sending': + return SENDING_STATUSES + elif filter_args['status'] == 'delivered': + return DELIVERED_STATUSES + elif filter_args['status'] == 'failed': + return FAILURE_STATUSES + else: + current_app.logger.info('Unrecognised status filter: {}'.format(filter_args['status'])) + return REQUESTED_STATUSES + + +@main.route("/services//one-off-notification/") +@login_required +@user_has_permissions('view_activity', admin_override=True) +def view_notification(service_id, notification_id): + notification = notification_api_client.get_notification(service_id, notification_id) + return render_template( + 'views/notifications/notification.html', + finished=(notification['status'] in (DELIVERED_STATUSES + FAILURE_STATUSES)), + uploaded_file_name='Report', + template=get_template( + notification['template'], + current_service, + letter_preview_url=url_for( + '.view_template_version_preview', + service_id=service_id, + template_id=notification['template']['id'], + version=notification['template_version'], + filetype='png', + ), + ), + status=request.args.get('status'), + updates_url=url_for( + ".view_notification_updates", + service_id=service_id, + notification_id=notification['id'], + status=request.args.get('status'), + help=get_help_argument() + ), + partials=get_single_notification_partials(notification), + help=get_help_argument() + ) + + +@main.route("/services//one-off-notification/.json") +@user_has_permissions('view_activity', admin_override=True) +def view_notification_updates(service_id, notification_id): + return jsonify(**get_single_notification_partials( + notification_api_client.get_notification(service_id, notification_id) + )) + + +def _get_single_notification_counts(notification, help_argument): + return [ + ( + label, + query_param, + url_for( + ".view_notification", + service_id=notification['service'], + notification_id=notification['id'], + status=query_param, + help=help_argument + ), + count + ) for label, query_param, count in [ + [ + 'total', '', + 1 + ], + [ + 'sending', 'sending', + int(notification['status'] in SENDING_STATUSES) + ], + [ + 'delivered', 'delivered', + int(notification['status'] in DELIVERED_STATUSES) + ], + [ + 'failed', 'failed', + int(notification['status'] in FAILURE_STATUSES) + ] + ] + ] + + +def get_single_notification_partials(notification): + status_args = get_status_arg(request.args) + + return { + 'counts': render_template( + 'partials/count.html', + counts=_get_single_notification_counts(notification, request.args.get('help', 0)), + status=status_args + ), + 'notifications': render_template( + 'partials/notifications/notifications.html', + notification=notification, + more_than_one_page=False, + percentage_complete=100, + time_left=get_time_left(notification['created_at']), + ), + 'status': render_template( + 'partials/notifications/status.html', + notification=notification + ), + } diff --git a/app/notify_client/notification_api_client.py b/app/notify_client/notification_api_client.py index 88ee80e21..bea8506d9 100644 --- a/app/notify_client/notification_api_client.py +++ b/app/notify_client/notification_api_client.py @@ -55,7 +55,5 @@ class NotificationApiClient(NotifyAdminAPIClient): params=params ) - def get_notification(self, service_id, notification_id): - return self.get( - url='/service/{}/notifications/{}'.format(service_id, notification_id) - ) + def get_notification(self, service_id, notification_id):m + return self.get(url='/service/{}/notifications/{}'.format(service_id, notification_id)) diff --git a/app/templates/partials/jobs/count.html b/app/templates/partials/count.html similarity index 100% rename from app/templates/partials/jobs/count.html rename to app/templates/partials/count.html diff --git a/app/templates/partials/notifications/notifications.html b/app/templates/partials/notifications/notifications.html new file mode 100644 index 000000000..6e2be2049 --- /dev/null +++ b/app/templates/partials/notifications/notifications.html @@ -0,0 +1,39 @@ +{% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading, notification_status_field %} +{% from "components/page-footer.html" import page_footer %} + +
+
+ + {% if not help %} +

+ Download this report +   + {{ time_left }} +

+ {% endif %} + + {% call(item, row_number) list_table( + [notification], + caption=None, + caption_visible=False, + empty_message=None, + field_headings=[ + 'Recipient', + 'Status' + ], + field_headings_visible=False + ) %} + {% call row_heading() %} +

{{ item.to }}

+ {% endcall %} + {{ notification_status_field(item) }} + {% endcall %} + + {% if more_than_one_page %} + + {% endif %} + +
+
diff --git a/app/templates/partials/notifications/status.html b/app/templates/partials/notifications/status.html new file mode 100644 index 000000000..481b32446 --- /dev/null +++ b/app/templates/partials/notifications/status.html @@ -0,0 +1,6 @@ +
+

+ Sent {% if notification.created_by %}by {{ notification.created_by.name }} {% endif %} + on {{ notification.created_at|format_datetime_short }} +

+
diff --git a/app/templates/views/notifications/notification.html b/app/templates/views/notifications/notification.html new file mode 100644 index 000000000..f4936fb53 --- /dev/null +++ b/app/templates/views/notifications/notification.html @@ -0,0 +1,27 @@ +{% extends "withnav_template.html" %} +{% from "components/banner.html" import banner %} +{% from "components/ajax-block.html" import ajax_block %} +{% from "components/page-footer.html" import page_footer %} + +{% block service_page_title %} + Report +{% endblock %} + +{% block maincolumn_content %} + +

+ Report +

+ + {{ template|string }} + + {{ ajax_block(partials, updates_url, 'status', finished=finished) }} + {{ ajax_block(partials, updates_url, 'counts', finished=finished) }} + {{ ajax_block(partials, updates_url, 'notifications', finished=finished) }} + + {{ page_footer( + secondary_link=url_for('.view_template', service_id=current_service.id, template_id=template.id), + secondary_link_text='Back to {}'.format(template.name) + ) }} + +{% endblock %} diff --git a/app/utils.py b/app/utils.py index 5cec5e67c..309427e72 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,11 +1,13 @@ import re import csv -from io import StringIO, BytesIO +from io import StringIO from os import path from functools import wraps import unicodedata -from datetime import datetime +from datetime import datetime, timedelta, timezone +import dateutil +import ago from flask import ( abort, current_app, @@ -15,6 +17,11 @@ from flask import ( url_for ) from flask_login import current_user +import pyexcel +import pyexcel.ext.io +import pyexcel.ext.xls +import pyexcel.ext.xlsx +import pyexcel.ext.ods3 from notifications_utils.template import ( SMSPreviewTemplate, @@ -23,12 +30,6 @@ from notifications_utils.template import ( LetterPreviewTemplate, ) -import pyexcel -import pyexcel.ext.io -import pyexcel.ext.xls -import pyexcel.ext.xlsx -import pyexcel.ext.ods3 - SENDING_STATUSES = ['created', 'pending', 'sending'] DELIVERED_STATUSES = ['delivered', 'sent'] @@ -144,13 +145,31 @@ def generate_notifications_csv(**kwargs): notification['status'], notification['created_at'] ] - line = ','.join([str(i) for i in values]) + '\n' + line = ','.join(str(i) for i in values) + '\n' yield line if notifications_resp['links'].get('next'): kwargs['page'] += 1 else: return + raise Exception("Should never reach here") + + +def generate_single_notification_csv(notification): + fieldnames = ['Recipient', 'Template', 'Type', 'Status', 'Time'] + yield ','.join(fieldnames) + '\n' + + values = [ + notification['to'], + notification['template']['name'], + notification['template']['template_type'], + notification['status'], + notification['created_at'] + ] + line = ','.join(str(i) for i in values) + '\n' + yield line + + return def get_page_from_request(): @@ -301,3 +320,16 @@ def get_current_financial_year(): current_month = int(now.strftime('%-m')) current_year = int(now.strftime('%Y')) return current_year if current_month > 3 else current_year - 1 + + +def get_time_left(created_at): + return ago.human( + ( + datetime.now(timezone.utc).replace(hour=23, minute=59, second=59) + ) - ( + dateutil.parser.parse(created_at) + timedelta(days=8) + ), + future_tense='Data available for {}', + past_tense='Data no longer available', # No-one should ever see this + precision=1 + )