diff --git a/app/assets/stylesheets/components/table.scss b/app/assets/stylesheets/components/table.scss index 3b4a4a49b..d69b25af5 100644 --- a/app/assets/stylesheets/components/table.scss +++ b/app/assets/stylesheets/components/table.scss @@ -49,6 +49,10 @@ } } + .big-number-label { + padding-bottom: 0; + } + } .template-statistics-table { diff --git a/app/assets/stylesheets/components/vendor/previous-next-navigation.scss b/app/assets/stylesheets/components/vendor/previous-next-navigation.scss index 8fc2213a0..5db0b9be8 100644 --- a/app/assets/stylesheets/components/vendor/previous-next-navigation.scss +++ b/app/assets/stylesheets/components/vendor/previous-next-navigation.scss @@ -34,6 +34,7 @@ $is-ie: false !default; margin-bottom: $gutter; margin-left: -$gutter-half; margin-right: -$gutter-half; + overflow: hidden; ul { margin: 0; diff --git a/app/assets/stylesheets/views/dashboard.scss b/app/assets/stylesheets/views/dashboard.scss index b300d5be2..8faef80c7 100644 --- a/app/assets/stylesheets/views/dashboard.scss +++ b/app/assets/stylesheets/views/dashboard.scss @@ -56,6 +56,18 @@ margin-top: -10px; } + &-filename-large { + @include bold-24; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-bottom: 30px; + padding-top: 10px; + margin-bottom: -30px; + margin-top: -10px; + } + &-filename-unlinked { @include core-19; } diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py index 65edfcfc2..a6b32f28d 100644 --- a/app/main/views/uploads.py +++ b/app/main/views/uploads.py @@ -46,20 +46,22 @@ def uploads(service_id): uploads = current_service.get_page_of_uploads(page=request.args.get('page')) prev_page = None - if uploads.next_page: + if uploads.prev_page: prev_page = generate_previous_dict('main.uploads', service_id, uploads.current_page) next_page = None - if uploads.prev_page: + if uploads.next_page: next_page = generate_next_dict('main.uploads', service_id, uploads.current_page) + if uploads.current_page == 1: + listed_uploads = current_service.scheduled_jobs + uploads + else: + listed_uploads = uploads + return render_template( 'views/jobs/jobs.html', - jobs=uploads, + jobs=listed_uploads, prev_page=prev_page, next_page=next_page, - show_scheduled_jobs=( - uploads.current_page == 1 and current_service.scheduled_jobs - ), ) diff --git a/app/models/__init__.py b/app/models/__init__.py index 4f8850e9d..3a01e8a81 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -75,6 +75,9 @@ class ModelList(ABC, Sequence): def __add__(self, other): return list(self) + list(other) + def __radd__(self, other): + return list(other) + list(self) + class InviteTokenError(Exception): pass diff --git a/app/models/job.py b/app/models/job.py index 5cd92c3a0..02d40159e 100644 --- a/app/models/job.py +++ b/app/models/job.py @@ -30,6 +30,8 @@ class Job(JSONModel): 'processing_started', 'notification_count', 'created_by', + 'template_type', + 'recipient', } @classmethod @@ -56,6 +58,10 @@ class Job(JSONModel): def upload_type(self): return self._dict.get('upload_type') + @property + def pdf_letter(self): + return self.upload_type == 'letter' + @property def processing_started(self): if not self._dict.get('processing_started'): @@ -132,10 +138,6 @@ class Job(JSONModel): version=self.template_version, )['data'] - @property - def template_type(self): - return self.template['template_type'] - @property def percentage_complete(self): return self.notifications_requested / self.notification_count * 100 diff --git a/app/templates/partials/jobs/notifications.html b/app/templates/partials/jobs/notifications.html index 842a9d805..86c52fb57 100644 --- a/app/templates/partials/jobs/notifications.html +++ b/app/templates/partials/jobs/notifications.html @@ -25,21 +25,21 @@
{% endif %} - {% if template.template_type == 'letter' %} + {% if job.template_type == 'letter' %}
{% endif %} {% if job.still_processing %} -

+

Report is {{ "{:.0f}%".format(job.percentage_complete * 0.99) }} complete…

{% elif notifications %} -

+

Download this report{{ time_left }}

{% endif %} - {% if template.template_type == 'letter' %} + {% if job.template_type == 'letter' %}
{% endif %} diff --git a/app/templates/views/dashboard/_jobs.html b/app/templates/views/dashboard/_jobs.html index 3d4c9bba4..cc9f84ac6 100644 --- a/app/templates/views/dashboard/_jobs.html +++ b/app/templates/views/dashboard/_jobs.html @@ -1,5 +1,6 @@ {% from "components/table.html" import list_table, field, right_aligned_field_heading, row_heading %} {% from "components/big-number.html" import big_number -%} +{% from "components/message-count-label.html" import message_count_label -%}
{% call(item, row_number) list_table( @@ -11,37 +12,79 @@ ), field_headings=[ 'File', - 'Sending', - 'Delivered', - 'Failed' + 'Status' ], - field_headings_visible=True if jobs else False + field_headings_visible=False ) %} {% call row_heading() %}
{% if item.upload_type == 'letter' %} - {{ item.original_file_name }} + {{ item.original_file_name }} {% else %} - {{ item.original_file_name }} + {{ item.original_file_name }} {% endif %} - - Sent {{ - (item.scheduled_for or item.created_at)|format_datetime_relative - }} - + {% if item.scheduled %} + + Sending {{ + item.scheduled_for|format_datetime_relative + }} + + {% else %} + + Sent {{ + (item.scheduled_for or item.created_at)|format_datetime_relative + }} + + {% endif %} +
{% endcall %} {% call field() %} - {{ big_number( - item.notifications_sending, - smallest=True - ) }} - {% endcall %} - {% call field() %} - {{ big_number(item.notifications_delivered, smallest=True) }} - {% endcall %} - {% call field(status='error' if item.high_failure_rate else '') %} - {{ big_number(item.notifications_failed, smallest=True) }} + {% if item.scheduled %} + {{ big_number( + item.notification_count, + smallest=True, + label=message_count_label( + item.notification_count, + item.template_type, + suffix='waiting to send' + ) + ) }} + {% elif item.template_type == 'letter' %} + {{ big_number( + item.notification_count, + smallest=True, + label=message_count_label( + item.notification_count, + item.template_type, + suffix='' + ) + ) }} + {% elif item.pdf_letter %} +

+ {% for line in item.recipient.splitlines() %} + {% if loop.index < 3 %} + {{ line }}
+ {% endif %} + {% endfor %} +

+ {% else %} +
+
+ {{ big_number( + item.notifications_sending, + smallest=True, + label='sending', + ) }} +
+
+ {{ big_number(item.notifications_delivered, smallest=True, label='delivered') }} +
+
+ {{ big_number(item.notifications_failed, smallest=True, label='failed') }} +
+ + {% endif %} {% endcall %} {% endcall %}
diff --git a/app/templates/views/jobs/jobs.html b/app/templates/views/jobs/jobs.html index b7d7088c5..0c14dce42 100644 --- a/app/templates/views/jobs/jobs.html +++ b/app/templates/views/jobs/jobs.html @@ -7,7 +7,7 @@ {% endblock %} {% block maincolumn_content %} -

Uploads

+

Uploads

{% if show_scheduled_jobs %} {% with hide_heading = True %} @@ -18,11 +18,17 @@ {% include 'views/dashboard/_jobs.html' %} {% endif %} {% if not jobs and not show_scheduled_jobs %} - {# - `_jobs.html` will show the ‘You have no jobs’ message when - passed an empty list of jobs - #} - {% include 'views/dashboard/_jobs.html' %} +

+ You have not uploaded any files yet. +

+ {% if current_service.has_permission('upload_letters') %} +

+ Upload a letter and Notify will print, pack and post it for you. +

+ {% endif %} +

+ To upload a list of contact details, first choose a template. +

{% endif %} {{ previous_next_navigation(prev_page, next_page) }} {% if current_service.can_upload_letters and current_user.has_permissions('send_messages') %} diff --git a/tests/__init__.py b/tests/__init__.py index 70f04ed12..eaaf86886 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -354,6 +354,7 @@ def job_json( job_id=None, template_id=None, template_version=1, + template_type='sms', created_at=None, bucket_name='', original_file_name="thisisatest.csv", @@ -375,6 +376,7 @@ def job_json( 'service': service_id, 'template': template_id, 'template_version': template_version, + 'template_type': template_type, 'original_file_name': original_file_name, 'created_at': created_at, 'notification_count': notification_count, diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 9e47e946e..b9fb1b01c 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -937,8 +937,11 @@ def test_should_show_recent_jobs_on_dashboard( )): assert filename in table_rows[index].find_all('th')[0].text assert 'Sent today at 11:09' in table_rows[index].find_all('th')[0].text - for column_index, count in enumerate((1, 0, 0)): - assert table_rows[index].find_all('td')[column_index].text.strip() == str(count) + assert normalize_spaces( + table_rows[index].select_one('td').text + ) == ( + '1 sending 0 delivered 0 failed' + ) @pytest.mark.parametrize('extra_permissions', ( diff --git a/tests/app/main/views/test_jobs.py b/tests/app/main/views/test_jobs.py index 2a9e7b69e..9dfbd5b9a 100644 --- a/tests/app/main/views/test_jobs.py +++ b/tests/app/main/views/test_jobs.py @@ -21,23 +21,23 @@ from tests.conftest import ( @pytest.mark.parametrize('user, expected_rows', [ (create_active_user_with_permissions(), ( ( - 'File Sending Delivered Failed' + 'File Status' ), ( 'export 1/1/2016.xls ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'all email addresses.xlsx ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'applicants.ods ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'thisisatest.csv ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), )), (create_active_caseworking_user(), ( @@ -53,23 +53,23 @@ from tests.conftest import ( 'Sending 1 January 2016 at 11:09pm 1' ), ( - 'File Sending Delivered Failed' + 'File Status' ), ( 'export 1/1/2016.xls ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'all email addresses.xlsx ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'applicants.ods ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'thisisatest.csv ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), )), ]) @@ -123,23 +123,23 @@ def test_jobs_page_doesnt_show_scheduled_on_page_2( for index, row in enumerate(( ( - 'File Sending Delivered Failed' + 'File Status' ), ( 'export 1/1/2016.xls ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'all email addresses.xlsx ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'applicants.ods ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), ( 'thisisatest.csv ' - 'Sent today at 12:12pm 1 0 0' + 'Sent today at 12:12pm 1 sending 0 delivered 0 failed' ), )): assert normalize_spaces(page.select('tr')[index].text) == row @@ -398,7 +398,7 @@ def test_should_show_old_job( def test_should_show_letter_job( client_request, mock_get_service_letter_template, - mock_get_job, + mock_get_letter_job, mock_get_service_data_retention, fake_uuid, mocker, @@ -460,7 +460,7 @@ def test_should_show_letter_job( def test_should_show_letter_job_with_banner_after_sending_before_1730( client_request, mock_get_service_letter_template, - mock_get_job, + mock_get_letter_job, mock_get_notifications, mock_get_service_data_retention, fake_uuid, @@ -483,7 +483,7 @@ def test_should_show_letter_job_with_banner_after_sending_before_1730( def test_should_show_letter_job_with_banner_when_there_are_multiple_CSV_rows( client_request, mock_get_service_letter_template, - mock_get_job_in_progress, + mock_get_letter_job_in_progress, mock_get_notifications, mock_get_service_data_retention, fake_uuid, @@ -506,7 +506,7 @@ def test_should_show_letter_job_with_banner_when_there_are_multiple_CSV_rows( def test_should_show_letter_job_with_banner_after_sending_after_1730( client_request, mock_get_service_letter_template, - mock_get_job, + mock_get_letter_job, mock_get_notifications, mock_get_service_data_retention, fake_uuid, @@ -601,7 +601,8 @@ def test_should_cancel_letter_job( active_user_with_permissions, job_id=job_id, created_at="2019-06-20T15:30:00.000001+00:00", - job_status="finished" + job_status="finished", + template_type="letter", ) mocker.patch('app.job_api_client.get_job', side_effect=[{"data": job}]) notifications_json = notification_json(SERVICE_ONE_ID, job=job, status="created", template_type="letter") @@ -819,7 +820,7 @@ def test_time_left(job_created_at, expected_message): def test_should_show_letter_job_with_first_class_if_notifications_are_first_class( client_request, mock_get_service_letter_template, - mock_get_job, + mock_get_letter_job, mock_get_service_data_retention, fake_uuid, mocker, @@ -840,7 +841,7 @@ def test_should_show_letter_job_with_first_class_if_notifications_are_first_clas def test_should_show_letter_job_with_first_class_if_no_notifications( client_request, service_one, - mock_get_job, + mock_get_letter_job, fake_uuid, mock_get_notifications_with_no_notifications, mock_get_service_data_retention, diff --git a/tests/app/main/views/test_uploads.py b/tests/app/main/views/test_uploads.py index cbb7a5396..943e5b205 100644 --- a/tests/app/main/views/test_uploads.py +++ b/tests/app/main/views/test_uploads.py @@ -39,10 +39,13 @@ def test_no_upload_letters_button_without_permission( @pytest.mark.parametrize('extra_permissions, expected_empty_message', ( (['letter'], ( - 'You have not uploaded any files yet' + 'You have not uploaded any files yet. ' + 'To upload a list of contact details, first choose a template.' )), (['letter', 'upload_letters'], ( - 'Upload a letter and Notify will print, pack and post it for you.' + 'You have not uploaded any files yet. ' + 'Upload a letter and Notify will print, pack and post it for you. ' + 'To upload a list of contact details, first choose a template.' )), )) def test_get_upload_hub_with_no_uploads( @@ -56,9 +59,9 @@ def test_get_upload_hub_with_no_uploads( mocker.patch('app.job_api_client.get_jobs', return_value={'data': []}) service_one['permissions'] += extra_permissions page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) - assert normalize_spaces( - page.select_one('.table-empty-message').text - ) == expected_empty_message + assert normalize_spaces(' '.join( + paragraph.text for paragraph in page.select('main p') + )) == expected_empty_message assert not page.select('.file-list-filename') @@ -76,13 +79,32 @@ def test_get_upload_hub_page( 'main.upload_letter', service_id=SERVICE_ONE_ID ) - assert page.find_all( - 'a', {'class': 'file-list-filename'} - )[0].attrs['href'] == '/services/{}/jobs/job_id_1'.format(SERVICE_ONE_ID) + uploads = page.select('tbody tr') - assert page.find_all( - 'a', {'class': 'file-list-filename'} - )[1].attrs['href'] == '/services/{}/notification/letter_id_1'.format(SERVICE_ONE_ID) + assert normalize_spaces(uploads[0].text.strip()) == ( + 'some.csv ' + 'Sent 1 January 2016 at 11:09am ' + '0 sending 8 delivered 2 failed' + ) + assert uploads[0].select_one('a.file-list-filename-large')['href'] == ( + '/services/{}/jobs/job_id_1'.format(SERVICE_ONE_ID) + ) + + assert normalize_spaces(uploads[1].text.strip()) == ( + 'some.pdf ' + 'Sent 1 January 2016 at 11:09am ' + 'Firstname Lastname ' + '123 Example Street' + ) + assert normalize_spaces(str(uploads[1].select_one('.govuk-body'))) == ( + '

' + 'Firstname Lastname
' + '123 Example Street
' + '

' + ) + assert uploads[1].select_one('a.file-list-filename-large')['href'] == ( + '/services/{}/notification/letter_id_1'.format(SERVICE_ONE_ID) + ) def test_get_upload_letter(client_request): @@ -643,15 +665,35 @@ def test_uploads_page_shows_scheduled_jobs( normalize_spaces(row.text) for row in page.select('tr') ] == [ ( - 'File Messages to be sent' + 'File Status' ), ( 'send_me_later.csv ' - 'Sending 1 January 2016 at 11:09am 1' + 'Sending 1 January 2016 at 11:09am ' + '1 text message waiting to send' ), ( 'even_later.csv ' - 'Sending 1 January 2016 at 11:09pm 1' + 'Sending 1 January 2016 at 11:09pm ' + '1 text message waiting to send' ), ] assert not page.select('.table-empty-message') + + +def test_get_uploads_shows_pagination( + client_request, + active_user_with_permissions, + mock_get_jobs, + mock_get_uploads, +): + page = client_request.get('main.uploads', service_id=SERVICE_ONE_ID) + + assert normalize_spaces(page.select_one('.next-page').text) == ( + 'Next page ' + 'page 2' + ) + assert normalize_spaces(page.select_one('.previous-page').text) == ( + 'Previous page ' + 'page 0' + ) diff --git a/tests/conftest.py b/tests/conftest.py index 3b3cd7ed6..f01c9150c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1656,6 +1656,14 @@ def mock_get_job(mocker, api_user_active): return mocker.patch('app.job_api_client.get_job', side_effect=_get_job) +@pytest.fixture(scope='function') +def mock_get_letter_job(mocker, api_user_active): + def _get_job(service_id, job_id): + return {"data": job_json(service_id, api_user_active, job_id=job_id, template_type='letter')} + + return mocker.patch('app.job_api_client.get_job', side_effect=_get_job) + + @pytest.fixture def mock_get_job_doesnt_exist(mocker): def _get_job(service_id, job_id): @@ -1705,6 +1713,20 @@ def mock_get_job_in_progress(mocker, api_user_active): return mocker.patch('app.job_api_client.get_job', side_effect=_get_job) +@pytest.fixture(scope='function') +def mock_get_letter_job_in_progress(mocker, api_user_active): + def _get_job(service_id, job_id): + return {"data": job_json( + service_id, api_user_active, job_id=job_id, + notification_count=10, + notifications_requested=5, + job_status='processing', + template_type='letter', + )} + + return mocker.patch('app.job_api_client.get_job', side_effect=_get_job) + + @pytest.fixture(scope='function') def mock_has_jobs(mocker): mocker.patch('app.job_api_client.has_jobs', return_value=True) @@ -1759,13 +1781,22 @@ def mock_get_uploads(mocker, api_user_active): 'notification_count': 10, 'created_at': '2016-01-01 11:09:00.061258', 'statistics': [{'count': 8, 'status': 'delivered'}, {'count': 2, 'status': 'temporary-failure'}], - 'upload_type': 'job'}, + 'upload_type': 'job', + 'template_type': 'sms', + 'recipient': None}, {'id': 'letter_id_1', 'original_file_name': 'some.pdf', 'notification_count': 1, 'created_at': '2016-01-01 11:09:00.061258', 'statistics': [{'count': 1, 'status': 'delivered'}], - 'upload_type': 'letter'} + 'upload_type': 'letter', + 'template_type': None, + 'recipient': ( + 'Firstname Lastname\n' + '123 Example Street\n' + 'City of Town\n' + 'XM4 5QQ' + )} ] return { 'data': uploads,