From 2800b0a0c3ac14f339a522cd871dd18eee7da0b8 Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Mon, 11 May 2020 10:52:43 +0100 Subject: [PATCH] Add page to show all uploaded letters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Because we won’t be showing uploaded letters individually on the uploads page any more we need a way of listing them. This should be by printing day, to match how we’re grouping them on the uploads page. This code reuses the notifications.html template, but flips the precedence of the filename and recipient because I reckon when you’re looking at uploads you’re thinking filename-first. --- app/__init__.py | 2 + app/main/views/uploads.py | 56 +++++++- app/navigation.py | 4 + app/notify_client/upload_api_client.py | 18 +++ .../views/uploads/uploaded-letters.html | 37 +++++ tests/app/main/views/test_uploads.py | 132 ++++++++++++++++++ tests/conftest.py | 115 +++++++++++++++ 7 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 app/notify_client/upload_api_client.py create mode 100644 app/templates/views/uploads/uploaded-letters.html diff --git a/app/__init__.py b/app/__init__.py index d84939125..5f637cdd8 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -87,6 +87,7 @@ from app.notify_client.template_folder_api_client import ( from app.notify_client.template_statistics_api_client import ( template_statistics_client, ) +from app.notify_client.upload_api_client import upload_api_client from app.notify_client.user_api_client import user_api_client from app.url_converters import ( LetterFileExtensionConverter, @@ -160,6 +161,7 @@ def create_app(application): status_api_client, template_folder_api_client, template_statistics_client, + upload_api_client, user_api_client, # External API clients diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py index 263fd43bf..510c8fed7 100644 --- a/app/main/views/uploads.py +++ b/app/main/views/uploads.py @@ -25,7 +25,12 @@ from requests import RequestException from xlrd.biffh import XLRDError from xlrd.xldate import XLDateError -from app import current_service, notification_api_client, service_api_client +from app import ( + current_service, + notification_api_client, + service_api_client, + upload_api_client, +) from app.extensions import antivirus_client from app.main import main from app.main.forms import CsvUploadForm, LetterUploadPostageForm, PDFUploadForm @@ -42,7 +47,9 @@ from app.utils import ( generate_next_dict, generate_previous_dict, get_errors_for_csv, + get_letter_printing_statement, get_letter_validation_error, + get_page_from_request, get_sample_template, get_template, unicode_truncate, @@ -83,6 +90,53 @@ def uploads(service_id): ) +@main.route("/services//uploaded-letters/") +@user_has_permissions() +def uploaded_letters(service_id, letter_print_day): + page = get_page_from_request() + if page is None: + abort(404, "Invalid page argument ({}).".format(request.args.get('page'))) + uploaded_letters = upload_api_client.get_letters_by_service_and_print_day( + current_service.id, letter_print_day=letter_print_day, page=page, + ) + + prev_page = None + if uploaded_letters['links'].get('prev'): + prev_page = generate_previous_dict('.uploaded_letters', service_id, page, url_args={ + 'letter_print_day': letter_print_day + }) + next_page = None + if uploaded_letters['links'].get('next'): + next_page = generate_next_dict('.uploaded_letters', service_id, page, url_args={ + 'letter_print_day': letter_print_day + }) + return render_template( + 'views/uploads/uploaded-letters.html', + notifications=add_preview_of_content_uploaded_letters( + uploaded_letters['notifications'] + ), + prev_page=prev_page, + next_page=next_page, + show_pagination=True, + total=uploaded_letters['total'], + letter_printing_statement=get_letter_printing_statement( + 'created', + letter_print_day, + ), + letter_print_day=letter_print_day, + ) + + +def add_preview_of_content_uploaded_letters(notifications): + + for notification in notifications: + yield(dict( + preview_of_content=', '.join(notification.pop('to').splitlines()), + to=notification['client_reference'], + **notification + )) + + @main.route("/services//upload-letter", methods=['GET', 'POST']) @user_has_permissions('send_messages') def upload_letter(service_id): diff --git a/app/navigation.py b/app/navigation.py index acf2e9406..b3485ca20 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -331,6 +331,7 @@ class HeaderNavigation(Navigation): 'delete_contact_list', 'upload_letter', 'uploaded_letter_preview', + 'uploaded_letters', 'uploads', 'usage', 'view_job', @@ -407,6 +408,7 @@ class MainNavigation(Navigation): 'delete_contact_list', 'upload_letter', 'uploaded_letter_preview', + 'uploaded_letters', 'uploads', 'view_job', 'view_jobs', @@ -696,6 +698,7 @@ class CaseworkNavigation(Navigation): 'delete_contact_list', 'upload_letter', 'uploaded_letter_preview', + 'uploaded_letters', 'uploads', }, } @@ -1248,6 +1251,7 @@ class OrgNavigation(Navigation): 'delete_contact_list', 'upload_letter', 'uploaded_letter_preview', + 'uploaded_letters', 'uploads', 'usage', 'usage_for_all_services', diff --git a/app/notify_client/upload_api_client.py b/app/notify_client/upload_api_client.py new file mode 100644 index 000000000..33dedd87c --- /dev/null +++ b/app/notify_client/upload_api_client.py @@ -0,0 +1,18 @@ +from app.notify_client import NotifyAdminAPIClient + + +class UploadApiClient(NotifyAdminAPIClient): + + def get_letters_by_service_and_print_day( + self, + service_id, + *, + letter_print_day, + page=1, + ): + return self.get( + url=f'/service/{service_id}/upload/uploaded-letters/{letter_print_day}?page={page}' + ) + + +upload_api_client = UploadApiClient() diff --git a/app/templates/views/uploads/uploaded-letters.html b/app/templates/views/uploads/uploaded-letters.html new file mode 100644 index 000000000..976859662 --- /dev/null +++ b/app/templates/views/uploads/uploaded-letters.html @@ -0,0 +1,37 @@ +{% extends "withnav_template.html" %} +{% from 'components/big-number.html' import big_number %} +{% from 'components/message-count-label.html' import message_count_label %} +{% from "components/page-header.html" import page_header %} + +{% block service_page_title %} + Uploaded letters +{% endblock %} + +{% block maincolumn_content %} + + {{ page_header( + 'Uploaded letters', + back_link=url_for('main.uploads', service_id=current_service.id) + ) }} + + {% if notifications %} + +

+ {{ total|format_thousands}} {{ message_count_label(total, 'letter', suffix='') }} +

+ +

+ {{ letter_printing_statement }} +

+ + {% else %} + +

+ No letters printing {{ letter_print_day }} +

+ + {% endif %} + + {% include 'views/activity/notifications.html' %} + +{% endblock %} diff --git a/tests/app/main/views/test_uploads.py b/tests/app/main/views/test_uploads.py index 98246762a..528ef0f17 100644 --- a/tests/app/main/views/test_uploads.py +++ b/tests/app/main/views/test_uploads.py @@ -134,6 +134,138 @@ def test_get_upload_hub_page( ) +@freeze_time('2020-02-02 14:00') +def test_get_uploaded_letters( + mocker, + client_request, + service_one, + mock_get_uploaded_letters, +): + page = client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02' + ) + assert normalize_spaces( + page.select_one('h1').text + ) == ( + 'Uploaded letters' + ) + assert normalize_spaces( + page.select('main p')[0].text + ) == ( + '1,234 letters' + ) + assert normalize_spaces( + page.select('main p')[1].text + ) == ( + 'Printing starts today at 5:30pm' + ) + + assert [ + normalize_spaces(row.text) + for row in page.select('tbody tr') + ] == [ + ( + 'Homer-Simpson.pdf ' + '742 Evergreen Terrace ' + '2 February at 1:59pm' + ), + ( + 'Kevin-McCallister.pdf ' + '671 Lincoln Avenue, Winnetka ' + '2 February at 12:59pm' + ), + ] + + next_page_link = page.select_one('a[rel=next]') + prev_page_link = page.select_one('a[rel=previous]') + assert next_page_link['href'] == url_for( + 'main.uploaded_letters', service_id=SERVICE_ONE_ID, letter_print_day='2020-02-02', page=2 + ) + assert normalize_spaces(next_page_link.text) == ( + 'Next page ' + 'page 2' + ) + assert prev_page_link['href'] == url_for( + 'main.uploaded_letters', service_id=SERVICE_ONE_ID, letter_print_day='2020-02-02', page=0 + ) + assert normalize_spaces(prev_page_link.text) == ( + 'Previous page ' + 'page 0' + ) + + mock_get_uploaded_letters.assert_called_once_with( + SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page=1, + ) + + +@freeze_time('2020-02-02 14:00') +def test_get_empty_uploaded_letters_page( + mocker, + client_request, + service_one, + mock_get_no_uploaded_letters, +): + page = client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02' + ) + page.select_one('main table') + + assert not page.select('tbody tr') + assert not page.select_one('a[rel=next]') + assert not page.select_one('a[rel=previous]') + + +@freeze_time('2020-02-02') +def test_get_uploaded_letters_passes_through_page_argument( + mocker, + client_request, + service_one, + mock_get_uploaded_letters, +): + client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page=99, + ) + mock_get_uploaded_letters.assert_called_once_with( + SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page=99, + ) + + +def test_get_uploaded_letters_404s_for_bad_page_arguments( + mocker, + client_request, +): + client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='2020-02-02', + page='one', + _expected_status=404, + ) + + +def test_get_uploaded_letters_404s_for_invalid_date( + mocker, + client_request, +): + client_request.get( + 'main.uploaded_letters', + service_id=SERVICE_ONE_ID, + letter_print_day='1234-56-78', + _expected_status=404, + ) + + def test_get_upload_letter(client_request): page = client_request.get('main.upload_letter', service_id=SERVICE_ONE_ID) diff --git a/tests/conftest.py b/tests/conftest.py index 15620aa71..48578599b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1828,6 +1828,121 @@ def mock_get_uploads(mocker, api_user_active): return mocker.patch('app.models.job.PaginatedUploads.client_method', side_effect=_get_uploads) +@pytest.fixture(scope='function') +def mock_get_uploaded_letters(mocker): + def _get_uploaded_letters(service_id, *, letter_print_day, page=1): + uploads = [ + { + "api_key": None, + "billable_units": 1, + "client_reference": "Homer-Simpson.pdf", + "created_at": "2020-02-02T13:59:00+00:00", + "created_by": { + "email_address": "test@example.com", + "id": "a1958d6b-955b-4f68-8847-cf43cd4b189a", + "name": "Test User" + }, + "document_download_count": None, + "id": "03e34025-be54-4d43-8e6a-fb1ea0fd1f29", + "international": False, + "job": None, + "job_row_number": None, + "key_name": None, + "key_type": "normal", + "normalised_to": None, + "notification_type": "letter", + "personalisation": {}, + "phone_prefix": None, + "postage": "second", + "rate_multiplier": None, + "reference": "dvla-reference", + "reply_to_text": None, + "sent_at": "2020-02-02T14:00:00+00:00", + "sent_by": None, + "service": "f995d8a3-4ece-4961-bbbd-b657b192648c", + "status": "delivered", + "template": { + "content": "", + "id": "673e4f67-7d9a-42b8-8e56-f3444aa2fbef", + "is_precompiled_letter": True, + "name": "Pre-compiled PDF", + "redact_personalisation": False, + "subject": "Pre-compiled PDF", + "template_type": "letter", + "version": 1 + }, + "to": "742 Evergreen Terrace", + "updated_at": "2020-02-02T14:00:00+00:00" + }, + { + "api_key": None, + "billable_units": 1, + "client_reference": "Kevin-McCallister.pdf", + "created_at": "2020-02-02T12:59:00+00:00", + "created_by": { + "email_address": "test@example.com", + "id": "a1958d6b-955b-4f68-8847-cf43cd4b189a", + "name": "Test User" + }, + "document_download_count": None, + "id": "03e34025-be54-4d43-8e6a-fb1ea0fd1f29", + "international": False, + "job": None, + "job_row_number": None, + "key_name": None, + "key_type": "normal", + "normalised_to": None, + "notification_type": "letter", + "personalisation": {}, + "phone_prefix": None, + "postage": "second", + "rate_multiplier": None, + "reference": "dvla-reference", + "reply_to_text": None, + "sent_at": "2020-02-02T14:00:00+00:00", + "sent_by": None, + "service": "f995d8a3-4ece-4961-bbbd-b657b192648c", + "status": "delivered", + "template": { + "content": "", + "id": "673e4f67-7d9a-42b8-8e56-f3444aa2fbef", + "is_precompiled_letter": True, + "name": "Pre-compiled PDF", + "redact_personalisation": False, + "subject": "Pre-compiled PDF", + "template_type": "letter", + "version": 1 + }, + "to": "671 Lincoln Avenue\nWinnetka", + "updated_at": "2020-02-02T14:00:00+00:00" + }, + ] + return { + 'notifications': uploads, + 'total': 1234, + 'links': { + 'prev': 'services/{}/uploads?page={}'.format(service_id, page - 1), + 'next': 'services/{}/uploads?page={}'.format(service_id, page + 1) + } + } + return mocker.patch( + 'app.main.views.uploads.upload_api_client.get_letters_by_service_and_print_day', + side_effect=_get_uploaded_letters, + ) + + +@pytest.fixture(scope='function') +def mock_get_no_uploaded_letters(mocker): + return mocker.patch( + 'app.main.views.uploads.upload_api_client.get_letters_by_service_and_print_day', + return_value={ + 'notifications': [], + 'total': 0, + 'links': {} + }, + ) + + @pytest.fixture(scope='function') def mock_get_no_uploads(mocker, api_user_active): mocker.patch(