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(