Date: Wed, 19 Feb 2020 17:18:03 +0000
Subject: [PATCH 03/18] =?UTF-8?q?Say=20=E2=80=98Uploaded=E2=80=99=20for=20?=
=?UTF-8?q?one-off=20precompiled=20letters?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This calls back to what the user has actively done. _Provided as a PDF_
is more passive and suitable for PDF letters sent using the API.
---
.../views/notifications/notification.html | 6 ++-
tests/app/main/views/test_notifications.py | 45 +++++++++++++++++++
2 files changed, 50 insertions(+), 1 deletion(-)
diff --git a/app/templates/views/notifications/notification.html b/app/templates/views/notifications/notification.html
index 825adc81f..8aa91cb08 100644
--- a/app/templates/views/notifications/notification.html
+++ b/app/templates/views/notifications/notification.html
@@ -17,7 +17,11 @@
) }}
{% if is_precompiled_letter %}
- Provided as PDF
+ {% if created_by %}
+ Uploaded
+ {% else %}
+ Provided as PDF
+ {% endif %}
{% else %}
{% if help %}
‘{{ template.name }}’
diff --git a/tests/app/main/views/test_notifications.py b/tests/app/main/views/test_notifications.py
index 10fc2d4d2..590e61c89 100644
--- a/tests/app/main/views/test_notifications.py
+++ b/tests/app/main/views/test_notifications.py
@@ -247,6 +247,51 @@ def test_notification_page_shows_page_for_letter_notification(
assert mock_page_count.call_args_list[0][1]['values'] == {'name': 'Jo'}
+@freeze_time("2020-01-01 00:00")
+def test_notification_page_shows_uploaded_letter(
+ client_request,
+ mocker,
+ fake_uuid,
+):
+ mocker.patch(
+ 'app.main.views.notifications.view_letter_notification_as_preview',
+ return_value=(b'foo', {
+ 'message': '',
+ 'invalid_pages': '[]',
+ 'page_count': '1'
+ })
+ )
+ mocker.patch(
+ 'app.main.views.notifications.pdf_page_count',
+ return_value=1
+ )
+ mocker.patch(
+ 'app.main.views.notifications.get_page_count_for_letter',
+ return_value=1,
+ )
+
+ notification = create_notification(
+ notification_status='created',
+ template_type='letter',
+ is_precompiled_letter=True,
+ sent_one_off=True,
+ )
+ mocker.patch('app.notification_api_client.get_notification', return_value=notification)
+
+ page = client_request.get(
+ 'main.view_notification',
+ service_id=SERVICE_ONE_ID,
+ notification_id=fake_uuid,
+ )
+
+ assert normalize_spaces(page.select('main p:nth-of-type(1)')[0].text) == (
+ 'Uploaded by Test User yesterday at midnight'
+ )
+ assert normalize_spaces(page.select('main p:nth-of-type(2)')[0].text) == (
+ 'Printing starts today at 5:30pm'
+ )
+
+
@freeze_time("2016-01-01 01:01")
@pytest.mark.parametrize('is_precompiled_letter, expected_p1, expected_p2, expected_postage', (
(
From 8a98c73b080c1dcc8c8c0059d1981b05180527f3 Mon Sep 17 00:00:00 2001
From: Tom Byers
Date: Thu, 20 Feb 2020 11:03:03 +0000
Subject: [PATCH 04/18] Make analytics send the same title for all pages
Sets it on the tracker which means this value gets
sent for each:
- pageview
- event
See: https://developers.google.com/analytics/devguides/collection/analyticsjs/pages
We can only test that this has been set on the
tracker, not how that effects what is sent to GA,
in the JS tests.
This change has been tested in-browser with the
Chrome Analytics Debugger. This revealed the
contents of what is in the beacon sent to GA and
allowed confirmation the title was being set
correctly.
See: https://chrome.google.com/webstore/detail/google-analytics-debugger/jnkmfdileelhofjcijamephohjechhna/related
---
app/assets/javascripts/analytics/analytics.js | 1 +
tests/javascripts/analytics/analytics.test.js | 1 +
tests/javascripts/analytics/init.test.js | 6 +++---
tests/javascripts/cookieSettings.test.js | 4 ++--
4 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/app/assets/javascripts/analytics/analytics.js b/app/assets/javascripts/analytics/analytics.js
index 711f6ec80..a776f93a2 100644
--- a/app/assets/javascripts/analytics/analytics.js
+++ b/app/assets/javascripts/analytics/analytics.js
@@ -15,6 +15,7 @@
window.ga('set', 'anonymizeIp', config.anonymizeIp);
window.ga('set', 'allowAdFeatures', config.allowAdFeatures);
window.ga('set', 'transport', config.transport);
+ window.ga('set', 'title', 'GOV.UK Notify');
};
diff --git a/tests/javascripts/analytics/analytics.test.js b/tests/javascripts/analytics/analytics.test.js
index ea4b91fe2..1f11544e8 100644
--- a/tests/javascripts/analytics/analytics.test.js
+++ b/tests/javascripts/analytics/analytics.test.js
@@ -52,6 +52,7 @@ describe("Analytics", () => {
expect(setUpArguments[1]).toEqual(['set', 'anonymizeIp', true]);
expect(setUpArguments[2]).toEqual(['set', 'allowAdFeatures', false]);
expect(setUpArguments[3]).toEqual(['set', 'transport', 'beacon']);
+ expect(setUpArguments[4]).toEqual(['set', 'title', 'GOV.UK Notify']);
});
diff --git a/tests/javascripts/analytics/init.test.js b/tests/javascripts/analytics/init.test.js
index 8384b826f..6bf7a31c8 100644
--- a/tests/javascripts/analytics/init.test.js
+++ b/tests/javascripts/analytics/init.test.js
@@ -111,10 +111,10 @@ describe("Analytics init", () => {
test("A pageview will be registered", () => {
- expect(window.ga.mock.calls.length).toEqual(5);
+ expect(window.ga.mock.calls.length).toEqual(6);
- // The first 4 calls configure the analytics tracker. All subsequent calls send data
- expect(window.ga.mock.calls[4]).toEqual(['send', 'pageview', '/privacy']);
+ // The first 5 calls configure the analytics tracker. All subsequent calls send data
+ expect(window.ga.mock.calls[5]).toEqual(['send', 'pageview', '/privacy']);
});
diff --git a/tests/javascripts/cookieSettings.test.js b/tests/javascripts/cookieSettings.test.js
index a6c6f1344..4bc85a213 100644
--- a/tests/javascripts/cookieSettings.test.js
+++ b/tests/javascripts/cookieSettings.test.js
@@ -231,8 +231,8 @@ describe("Cookie settings", () => {
expect(window.GOVUK.initAnalytics).toHaveBeenCalled();
expect(window.ga).toHaveBeenCalled();
- // the first 4 calls are configuration
- expect(window.ga.mock.calls[4]).toEqual(['send', 'pageview', '/privacy']);
+ // the first 5 calls are configuration
+ expect(window.ga.mock.calls[5]).toEqual(['send', 'pageview', '/privacy']);
});
From 74e70ed8bc30bac99dbaef04eda70c2aacc3bfc4 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Wed, 12 Feb 2020 14:03:51 +0000
Subject: [PATCH 05/18] Refactor summaries into model
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This lets us encapsulate some of the logic that’s currently cluttering
up the view/template layer.
---
app/main/views/dashboard.py | 4 ----
app/main/views/returned_letters.py | 5 ++---
app/models/service.py | 10 ++++++++++
app/templates/views/dashboard/_inbox.html | 12 ++++++------
4 files changed, 18 insertions(+), 13 deletions(-)
diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py
index 2ae4394b0..b66eedf8f 100644
--- a/app/main/views/dashboard.py
+++ b/app/main/views/dashboard.py
@@ -296,10 +296,6 @@ def get_dashboard_partials(service_id):
),
'inbox': render_template(
'views/dashboard/_inbox.html',
- inbound_sms_summary=(
- service_api_client.get_inbound_sms_summary(service_id)
- if current_service.has_permission('inbound_sms') else None
- ),
),
'totals': render_template(
'views/dashboard/_totals.html',
diff --git a/app/main/views/returned_letters.py b/app/main/views/returned_letters.py
index cdc06e464..bb81f3449 100644
--- a/app/main/views/returned_letters.py
+++ b/app/main/views/returned_letters.py
@@ -2,7 +2,7 @@ from collections import OrderedDict
from flask import render_template
-from app import service_api_client
+from app import current_service, service_api_client
from app.main import main
from app.utils import Spreadsheet, user_has_permissions
@@ -10,10 +10,9 @@ from app.utils import Spreadsheet, user_has_permissions
@main.route("/services//returned-letters")
@user_has_permissions('view_activity')
def returned_letter_summary(service_id):
- summary = service_api_client.get_returned_letter_summary(service_id)
return render_template(
'views/returned-letter-summary.html',
- data=summary,
+ data=current_service.returned_letter_summary,
)
diff --git a/app/models/service.py b/app/models/service.py
index 9e4fad965..c7b3f2737 100644
--- a/app/models/service.py
+++ b/app/models/service.py
@@ -480,6 +480,12 @@ class Service(JSONModel):
def has_inbound_number(self):
return bool(self.inbound_number)
+ @cached_property
+ def inbound_sms_summary(self):
+ if not self.has_permission('inbound_sms'):
+ return None
+ return service_api_client.get_inbound_sms_summary(self.id)
+
@cached_property
def all_template_folders(self):
return sorted(
@@ -660,3 +666,7 @@ class Service(JSONModel):
):
if test:
yield BASE + '_incomplete' + tag
+
+ @cached_property
+ def returned_letter_summary(self):
+ return service_api_client.get_returned_letter_summary(self.id)
diff --git a/app/templates/views/dashboard/_inbox.html b/app/templates/views/dashboard/_inbox.html
index 9d3586903..57a9d85b6 100644
--- a/app/templates/views/dashboard/_inbox.html
+++ b/app/templates/views/dashboard/_inbox.html
@@ -2,17 +2,17 @@
{% from "components/message-count-label.html" import message_count_label %}
- {% if inbound_sms_summary != None %}
-
+ {% if current_service.inbound_sms_summary != None %}
+
- {{ inbound_sms_summary.count|format_thousands }}
+ {{ current_service.inbound_sms_summary.count|format_thousands }}
- {{ message_count_label(inbound_sms_summary.count, 'sms', suffix='received') }}
+ {{ message_count_label(current_service.inbound_sms_summary.count, 'sms', suffix='received') }}
- {% if inbound_sms_summary.most_recent %}
+ {% if current_service.inbound_sms_summary.most_recent %}
- latest message {{ inbound_sms_summary.most_recent | format_delta }}
+ latest message {{ current_service.inbound_sms_summary.most_recent | format_delta }}
{% endif %}
From f369f76ae4860d3505a0924b7a2bdbdbc89a013e Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Wed, 12 Feb 2020 14:19:21 +0000
Subject: [PATCH 06/18] Count recently-returned letters on the dashboard
Currently you have no way of getting to the returned letter page. This
commit adds a link to it from the dashboard, following the pattern of
the new received text messages banner.
---
app/__init__.py | 17 +++
app/models/service.py | 25 ++++
app/templates/views/dashboard/_inbox.html | 13 ++
tests/app/main/views/test_accept_invite.py | 2 +
tests/app/main/views/test_dashboard.py | 160 +++++++++++++++++++--
tests/app/main/views/test_sign_out.py | 1 +
tests/conftest.py | 8 ++
7 files changed, 218 insertions(+), 8 deletions(-)
diff --git a/app/__init__.py b/app/__init__.py
index 4daf81b80..bffed68b2 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -356,6 +356,22 @@ def format_delta(date):
)
+def format_delta_days(date):
+ now = datetime.now(timezone.utc)
+ date = utc_string_to_aware_gmt_datetime(date)
+ delta = now - date
+ if date.strftime('%Y-%M-%D') == now.strftime('%Y-%M-%D'):
+ return "today"
+ if date.strftime('%Y-%M-%D') == (now - timedelta(days=1)).strftime('%Y-%M-%D'):
+ return "yesterday"
+ return ago.human(
+ delta,
+ precision=1,
+ future_tense='{} from now',
+ past_tense='{} ago',
+ )
+
+
def valid_phone_number(phone_number):
try:
validate_phone_number(phone_number)
@@ -744,6 +760,7 @@ def add_template_filters(application):
format_date_short,
format_datetime_relative,
format_delta,
+ format_delta_days,
format_notification_status,
format_notification_type,
format_notification_status_as_time,
diff --git a/app/models/service.py b/app/models/service.py
index c7b3f2737..132ac157c 100644
--- a/app/models/service.py
+++ b/app/models/service.py
@@ -1,4 +1,8 @@
+from datetime import datetime, timedelta
+
+from dateutil.parser import parse
from flask import abort, current_app
+from notifications_utils.timezones import local_timezone
from werkzeug.utils import cached_property
from app.models import JSONModel
@@ -670,3 +674,24 @@ class Service(JSONModel):
@cached_property
def returned_letter_summary(self):
return service_api_client.get_returned_letter_summary(self.id)
+
+ @property
+ def most_recent_returned_letter_report(self):
+ if not self.returned_letter_summary:
+ return None
+ return parse(
+ self.returned_letter_summary[0]['reported_at'] + " 00:00:00"
+ ).replace(tzinfo=local_timezone)
+
+ @property
+ def count_of_returned_letters_in_last_7_days(self):
+ seven_days_ago = (
+ datetime.now() - timedelta(days=7)
+ ).replace(
+ hour=0, minute=0, second=0
+ )
+ return sum(
+ report['returned_letter_count']
+ for report in self.returned_letter_summary
+ if parse(report['reported_at'] + " 00:00:00") >= seven_days_ago
+ )
diff --git a/app/templates/views/dashboard/_inbox.html b/app/templates/views/dashboard/_inbox.html
index 57a9d85b6..ebcd6fb84 100644
--- a/app/templates/views/dashboard/_inbox.html
+++ b/app/templates/views/dashboard/_inbox.html
@@ -17,4 +17,17 @@
{% endif %}
{% endif %}
+ {% if current_service.returned_letter_summary %}
+
+
+ {{ current_service.count_of_returned_letters_in_last_7_days|format_thousands }}
+
+
+ returned {{ message_count_label(current_service.count_of_returned_letters_in_last_7_days, 'letter', suffix='') }}
+
+
+ latest report {{ current_service.most_recent_returned_letter_report|format_delta_days }}
+
+
+ {% endif %}
diff --git a/tests/app/main/views/test_accept_invite.py b/tests/app/main/views/test_accept_invite.py
index cbd280ffe..b00b4209b 100644
--- a/tests/app/main/views/test_accept_invite.py
+++ b/tests/app/main/views/test_accept_invite.py
@@ -149,6 +149,7 @@ def test_accepting_invite_removes_invite_from_session(
mock_get_billable_units,
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
fake_uuid,
user,
landing_page_title,
@@ -470,6 +471,7 @@ def test_new_invited_user_verifies_and_added_to_service(
mock_get_service_statistics,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
mock_create_event,
mocker,
):
diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py
index fd95ee7b9..0aafafb88 100644
--- a/tests/app/main/views/test_dashboard.py
+++ b/tests/app/main/views/test_dashboard.py
@@ -143,7 +143,8 @@ def test_get_started(
mock_get_service_statistics,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
mocker.patch(
'app.template_statistics_client.get_template_statistics_for_service',
@@ -167,7 +168,8 @@ def test_get_started_is_hidden_once_templates_exist(
mock_get_service_statistics,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
mocker.patch(
'app.template_statistics_client.get_template_statistics_for_service',
@@ -184,6 +186,7 @@ def test_get_started_is_hidden_once_templates_exist(
def test_inbound_messages_not_visible_to_service_without_permissions(
client_request,
+ mocker,
service_one,
mock_get_service_templates_when_no_templates_exist,
mock_get_jobs,
@@ -191,7 +194,8 @@ def test_inbound_messages_not_visible_to_service_without_permissions(
mock_get_template_statistics,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
service_one['permissions'] = []
@@ -216,6 +220,7 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_messages(
mock_get_usage,
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
service_one['permissions'] = ['inbound_sms']
page = client_request.get(
@@ -242,6 +247,7 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
mock_get_usage,
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary_with_no_messages,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
service_one['permissions'] = ['inbound_sms']
page = client_request.get(
@@ -472,6 +478,126 @@ def test_download_inbox_strips_formulae(
assert expected_cell in response.get_data(as_text=True).split('\r\n')[1]
+def test_returned_letters_not_visible_if_service_has_no_returned_letters(
+ client_request,
+ mocker,
+ service_one,
+ mock_get_service_templates_when_no_templates_exist,
+ mock_get_jobs,
+ mock_get_service_statistics,
+ mock_get_template_statistics,
+ mock_get_usage,
+ mock_get_free_sms_fragment_limit,
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
+):
+ page = client_request.get(
+ 'main.service_dashboard',
+ service_id=SERVICE_ONE_ID,
+ )
+ assert not page.select('#total-returned-letters')
+
+
+@freeze_time('2020-01-10')
+def test_returned_letters_shows_count_of_recently_returned_letters(
+ client_request,
+ mocker,
+ service_one,
+ mock_get_service_templates_when_no_templates_exist,
+ mock_get_jobs,
+ mock_get_service_statistics,
+ mock_get_template_statistics,
+ mock_get_usage,
+ mock_get_free_sms_fragment_limit,
+ mock_get_inbound_sms_summary,
+):
+ mocker.patch(
+ 'app.service_api_client.get_returned_letter_summary',
+ return_value=[
+ # Today (should be counted)
+ {
+ 'returned_letter_count': 1000, 'reported_at': '2020-01-10'
+ },
+ # Just within the last 7 days (should be counted)
+ {
+ 'returned_letter_count': 3000, 'reported_at': '2020-01-3'
+ },
+ # Just after the last 7 days (should not be counted)
+ {
+ 'returned_letter_count': 2000, 'reported_at': '2020-01-2'
+ },
+ ],
+ )
+ page = client_request.get(
+ 'main.service_dashboard',
+ service_id=SERVICE_ONE_ID,
+ )
+ banner = page.select_one('#total-returned-letters')
+ assert normalize_spaces(
+ banner.text
+ ) == '4,000 returned letters latest report today'
+ assert banner['href'] == url_for(
+ 'main.returned_letter_summary', service_id=SERVICE_ONE_ID
+ )
+
+
+@pytest.mark.parametrize('reporting_date, expected_message', (
+ ('2020-02-02', (
+ '1 returned letter latest report today'
+ )),
+ ('2020-02-01', (
+ '1 returned letter latest report yesterday'
+ )),
+ ('2020-01-31', (
+ '1 returned letter latest report 2 days ago'
+ )),
+ ('2020-01-26', (
+ '1 returned letter latest report 7 days ago'
+ )),
+ ('2020-01-25', (
+ '0 returned letters latest report 8 days ago'
+ )),
+ ('2019-09-09', (
+ '0 returned letters latest report 146 days ago'
+ )),
+ ('2010-10-10', (
+ '0 returned letters latest report 9 years ago'
+ )),
+))
+@freeze_time('2020-02-02')
+def test_returned_letters_only_counts_recently_returned_letters(
+ client_request,
+ mocker,
+ service_one,
+ mock_get_service_templates_when_no_templates_exist,
+ mock_get_jobs,
+ mock_get_service_statistics,
+ mock_get_template_statistics,
+ mock_get_usage,
+ mock_get_free_sms_fragment_limit,
+ mock_get_inbound_sms_summary_with_no_messages,
+ reporting_date,
+ expected_message,
+):
+ mocker.patch(
+ 'app.service_api_client.get_returned_letter_summary',
+ return_value=[
+ {
+ 'returned_letter_count': 1, 'reported_at': reporting_date
+ },
+ ],
+ )
+ page = client_request.get(
+ 'main.service_dashboard',
+ service_id=SERVICE_ONE_ID,
+ )
+ banner = page.select_one('#total-returned-letters')
+ assert normalize_spaces(banner.text) == expected_message
+ assert banner['href'] == url_for(
+ 'main.returned_letter_summary', service_id=SERVICE_ONE_ID
+ )
+
+
def test_should_show_recent_templates_on_dashboard(
client_request,
mocker,
@@ -480,7 +606,8 @@ def test_should_show_recent_templates_on_dashboard(
mock_get_service_statistics,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
mock_template_stats = mocker.patch('app.template_statistics_client.get_template_statistics_for_service',
return_value=copy.deepcopy(stub_template_stats))
@@ -534,6 +661,7 @@ def test_should_not_show_recent_templates_on_dashboard_if_only_one_template_used
mock_get_usage,
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
stats,
):
mock_template_stats = mocker.patch(
@@ -687,7 +815,8 @@ def test_should_show_upcoming_jobs_on_dashboard(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
page = client_request.get(
'main.service_dashboard',
@@ -738,6 +867,7 @@ def test_correct_font_size_for_big_numbers(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
service_one,
permissions,
totals,
@@ -773,7 +903,8 @@ def test_should_show_recent_jobs_on_dashboard(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
page = client_request.get(
'main.service_dashboard',
@@ -981,6 +1112,7 @@ def test_menu_send_messages(
mock_get_usage,
mock_get_inbound_sms_summary,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
service_one['permissions'] = ['email', 'sms', 'letter', 'upload_letters']
@@ -1039,6 +1171,7 @@ def test_menu_manage_service(
mock_get_service_statistics,
mock_get_usage,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
mock_get_free_sms_fragment_limit,
):
with app_.test_request_context():
@@ -1070,6 +1203,7 @@ def test_menu_manage_api_keys(
mock_get_service_statistics,
mock_get_usage,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
mock_get_free_sms_fragment_limit,
):
with app_.test_request_context():
@@ -1099,6 +1233,7 @@ def test_menu_all_services_for_platform_admin_user(
mock_get_service_statistics,
mock_get_usage,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
mock_get_free_sms_fragment_limit,
):
with app_.test_request_context():
@@ -1130,7 +1265,8 @@ def test_route_for_service_permissions(
mock_get_service_statistics,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
with app_.test_request_context():
validate_route_permission(
@@ -1183,7 +1319,8 @@ def test_service_dashboard_updates_gets_dashboard_totals(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
- mock_get_inbound_sms_summary
+ mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
mocker.patch('app.main.views.dashboard.get_dashboard_totals', return_value={
'email': {'requested': 123, 'delivered': 0, 'failed': 0},
@@ -1368,6 +1505,7 @@ def test_should_show_all_jobs_with_valid_statuses(
mock_get_jobs,
mock_get_usage,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
mock_get_free_sms_fragment_limit,
):
logged_in_client.get(url_for('main.service_dashboard', service_id=SERVICE_ONE_ID))
@@ -1397,6 +1535,7 @@ def test_org_breadcrumbs_do_not_show_if_service_has_no_org(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
page = client_request.get('main.service_dashboard', service_id=SERVICE_ONE_ID)
@@ -1411,6 +1550,7 @@ def test_org_breadcrumbs_do_not_show_if_user_is_not_an_org_member(
active_caseworking_user,
client_request,
mock_get_template_folders,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
# active_caseworking_user is not an org member
@@ -1433,6 +1573,7 @@ def test_org_breadcrumbs_show_if_user_is_a_member_of_the_services_org(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
active_user_with_permissions,
client_request,
):
@@ -1460,6 +1601,7 @@ def test_org_breadcrumbs_do_not_show_if_user_is_a_member_of_the_services_org_but
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
active_user_with_permissions,
client_request,
):
@@ -1484,6 +1626,7 @@ def test_org_breadcrumbs_show_if_user_is_platform_admin(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
platform_admin_user,
platform_admin_client,
):
@@ -1515,6 +1658,7 @@ def test_should_show_usage_on_dashboard(
mock_get_jobs,
mock_get_usage,
mock_get_free_sms_fragment_limit,
+ mock_get_returned_letter_summary_with_no_returned_letters,
permissions,
):
service_one['permissions'] = permissions
diff --git a/tests/app/main/views/test_sign_out.py b/tests/app/main/views/test_sign_out.py
index 12e93f33b..2d1be4c2f 100644
--- a/tests/app/main/views/test_sign_out.py
+++ b/tests/app/main/views/test_sign_out.py
@@ -31,6 +31,7 @@ def test_sign_out_user(
mock_get_usage,
mock_get_free_sms_fragment_limit,
mock_get_inbound_sms_summary,
+ mock_get_returned_letter_summary_with_no_returned_letters,
):
with client_request.session_transaction() as session:
assert session.get('user_id') is not None
diff --git a/tests/conftest.py b/tests/conftest.py
index dfa844724..f08fd2d9c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3342,6 +3342,14 @@ def mock_get_service_history(mocker):
})
+@pytest.fixture(scope='function')
+def mock_get_returned_letter_summary_with_no_returned_letters(mocker):
+ return mocker.patch(
+ 'app.service_api_client.get_returned_letter_summary',
+ return_value=[],
+ )
+
+
def create_api_user_active(with_unique_id=False):
return {
'id': str(uuid4()) if with_unique_id else sample_uuid(),
From 9590643527643f264e2abe940a9a1f1769843140 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Fri, 14 Feb 2020 15:09:03 +0000
Subject: [PATCH 07/18] Use humanize for fuzzy time differences
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
It seems to do a bit better than ago (e.g. 4 months vs 146 days), and
looks like it’s maintained more often.
---
app/__init__.py | 17 +++--------------
requirements-app.txt | 1 +
requirements.txt | 5 +++--
tests/app/main/views/test_dashboard.py | 8 ++++----
4 files changed, 11 insertions(+), 20 deletions(-)
diff --git a/app/__init__.py b/app/__init__.py
index bffed68b2..6b7197f3c 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
from functools import partial
from time import monotonic
-import ago
+import humanize
import jinja2
from flask import (
Markup,
@@ -348,28 +348,17 @@ def format_delta(date):
return "just now"
if delta < timedelta(seconds=60):
return "in the last minute"
- return ago.human(
- delta,
- future_tense='{} from now', # No-one should ever see this
- past_tense='{} ago',
- precision=1
- )
+ return humanize.naturaltime(delta)
def format_delta_days(date):
now = datetime.now(timezone.utc)
date = utc_string_to_aware_gmt_datetime(date)
- delta = now - date
if date.strftime('%Y-%M-%D') == now.strftime('%Y-%M-%D'):
return "today"
if date.strftime('%Y-%M-%D') == (now - timedelta(days=1)).strftime('%Y-%M-%D'):
return "yesterday"
- return ago.human(
- delta,
- precision=1,
- future_tense='{} from now',
- past_tense='{} ago',
- )
+ return humanize.naturaltime(now - date)
def valid_phone_number(phone_number):
diff --git a/requirements-app.txt b/requirements-app.txt
index d2bd53f6d..ea804c360 100644
--- a/requirements-app.txt
+++ b/requirements-app.txt
@@ -2,6 +2,7 @@
# with package version changes made in requirements-app.txt
ago==0.0.93
+humanize==1.0.0
Flask==1.1.1
Flask-WTF==0.14.3
Flask-Login==0.4.1
diff --git a/requirements.txt b/requirements.txt
index 89e522545..9b7ca375f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,6 +4,7 @@
# with package version changes made in requirements-app.txt
ago==0.0.93
+humanize==1.0.0
Flask==1.1.1
Flask-WTF==0.14.3
Flask-Login==0.4.1
@@ -29,10 +30,10 @@ git+https://github.com/alphagov/notifications-utils.git@36.6.0#egg=notifications
git+https://github.com/alphagov/govuk-frontend-jinja.git@v0.5.1-alpha#egg=govuk-frontend-jinja==0.5.1-alpha
## The following requirements were added by pip freeze:
-awscli==1.17.15
+awscli==1.18.2
bleach==3.1.0
boto3==1.10.38
-botocore==1.14.15
+botocore==1.15.2
certifi==2019.11.28
chardet==3.0.4
Click==7.0
diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py
index 0aafafb88..6920b840c 100644
--- a/tests/app/main/views/test_dashboard.py
+++ b/tests/app/main/views/test_dashboard.py
@@ -262,9 +262,9 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
@pytest.mark.parametrize('index, expected_row', enumerate([
- '07900 900000 message-1 1 hour ago',
- '07900 900000 message-2 1 hour ago',
- '07900 900000 message-3 1 hour ago',
+ '07900 900000 message-1 an hour ago',
+ '07900 900000 message-2 an hour ago',
+ '07900 900000 message-3 an hour ago',
'07900 900002 message-4 3 hours ago',
'07900 900004 message-5 5 hours ago',
'07900 900006 message-6 7 hours ago',
@@ -558,7 +558,7 @@ def test_returned_letters_shows_count_of_recently_returned_letters(
'0 returned letters latest report 8 days ago'
)),
('2019-09-09', (
- '0 returned letters latest report 146 days ago'
+ '0 returned letters latest report 4 months ago'
)),
('2010-10-10', (
'0 returned letters latest report 9 years ago'
From 64074eed03e597c7e2342d15f8dd832fe6ff3bab Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Sat, 15 Feb 2020 18:29:58 +0000
Subject: [PATCH 08/18] =?UTF-8?q?Say=20=E2=80=981=20hour/month=20ago?=
=?UTF-8?q?=E2=80=99=20not=20=E2=80=98an=20hour/a=20month=20ago=E2=80=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
I think it read better without the indefinite article when it’s, for
example, placed alongside messages that read ‘2 hours ago’.
---
app/__init__.py | 13 +++++++++++--
tests/app/main/views/test_dashboard.py | 9 ++++++---
2 files changed, 17 insertions(+), 5 deletions(-)
diff --git a/app/__init__.py b/app/__init__.py
index 6b7197f3c..494c05720 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,5 +1,6 @@
import itertools
import os
+import re
import urllib
from datetime import datetime, timedelta, timezone
from functools import partial
@@ -338,6 +339,14 @@ def _format_datetime_short(datetime):
return datetime.strftime('%d %B').lstrip('0')
+def naturaltime_without_indefinite_article(date):
+ return re.sub(
+ 'an? (.*) ago',
+ lambda match: '1 {} ago'.format(match.group(1)),
+ humanize.naturaltime(date),
+ )
+
+
def format_delta(date):
delta = (
datetime.now(timezone.utc)
@@ -348,7 +357,7 @@ def format_delta(date):
return "just now"
if delta < timedelta(seconds=60):
return "in the last minute"
- return humanize.naturaltime(delta)
+ return naturaltime_without_indefinite_article(delta)
def format_delta_days(date):
@@ -358,7 +367,7 @@ def format_delta_days(date):
return "today"
if date.strftime('%Y-%M-%D') == (now - timedelta(days=1)).strftime('%Y-%M-%D'):
return "yesterday"
- return humanize.naturaltime(now - date)
+ return naturaltime_without_indefinite_article(now - date)
def valid_phone_number(phone_number):
diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py
index 6920b840c..3f09b4e18 100644
--- a/tests/app/main/views/test_dashboard.py
+++ b/tests/app/main/views/test_dashboard.py
@@ -262,9 +262,9 @@ def test_inbound_messages_shows_count_of_messages_when_there_are_no_messages(
@pytest.mark.parametrize('index, expected_row', enumerate([
- '07900 900000 message-1 an hour ago',
- '07900 900000 message-2 an hour ago',
- '07900 900000 message-3 an hour ago',
+ '07900 900000 message-1 1 hour ago',
+ '07900 900000 message-2 1 hour ago',
+ '07900 900000 message-3 1 hour ago',
'07900 900002 message-4 3 hours ago',
'07900 900004 message-5 5 hours ago',
'07900 900006 message-6 7 hours ago',
@@ -557,6 +557,9 @@ def test_returned_letters_shows_count_of_recently_returned_letters(
('2020-01-25', (
'0 returned letters latest report 8 days ago'
)),
+ ('2020-01-01', (
+ '0 returned letters latest report 1 month ago'
+ )),
('2019-09-09', (
'0 returned letters latest report 4 months ago'
)),
From d00e37541cee92391f09b022998634f03a23a7d4 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Mon, 17 Feb 2020 10:23:59 +0000
Subject: [PATCH 09/18] Make dashboard navigation active for returned letters
---
app/navigation.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/navigation.py b/app/navigation.py
index 503e47ebe..dd6aeecc4 100644
--- a/app/navigation.py
+++ b/app/navigation.py
@@ -354,6 +354,8 @@ class MainNavigation(Navigation):
'conversation',
'inbox',
'monthly',
+ 'returned_letter_summary',
+ 'returned_letters',
'service_dashboard',
'template_usage',
'view_job',
@@ -581,8 +583,6 @@ class MainNavigation(Navigation):
'resend_email_link',
'resend_email_verification',
'resume_service',
- 'returned_letter_summary',
- 'returned_letters',
'returned_letters_report',
'revalidate_email_sent',
'roadmap',
From 678c3df53ce9c77c9846e034bcbdae3ad5626da0 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Thu, 20 Feb 2020 11:48:34 +0000
Subject: [PATCH 10/18] Add some context to the list of reports
We reckon people need some context/expectation setting about what the
date of the report is.
---
.../views/returned-letter-summary.html | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/app/templates/views/returned-letter-summary.html b/app/templates/views/returned-letter-summary.html
index 5be1fc0b3..a68c2b267 100644
--- a/app/templates/views/returned-letter-summary.html
+++ b/app/templates/views/returned-letter-summary.html
@@ -1,5 +1,6 @@
{% from "components/table.html" import list_table, row_heading %}
{% from "components/message-count-label.html" import message_count_label %}
+{% from "components/page-header.html" import page_header %}
{% extends "withnav_template.html" %}
{% block service_page_title %}
@@ -7,9 +8,20 @@
{% endblock %}
{% block maincolumn_content %}
-
- Returned letters
-
+
+
+ {{ page_header(
+ 'Returned letters',
+ back_link=url_for('main.service_dashboard', service_id=current_service.id)
+ ) }}
+
+ Reports are published once a month.
+
+
+ You’ll only get a report if one or more of your letters is returned.
+
+
+
{% call(item, row_number) list_table(
data,
From 0e8143da0405e4da2335dbb2963d9abaf6fd9b25 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Thu, 20 Feb 2020 11:51:41 +0000
Subject: [PATCH 11/18] =?UTF-8?q?Remove=20=E2=80=98originally=E2=80=99=20f?=
=?UTF-8?q?rom=20returned=20letters=20report?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
We should remove Originally on the individual report pages.
It sounds like we're saying the letter has been sent more than once
since that date.
The dictionary definition for 'originally' as an adverb says:
> used to describe the situation that existed at the beginning of a
> particular period or activity, especially before something was changed
Examples given include:
> The book was originally published in 1935.
---
app/templates/views/returned-letters.html | 2 +-
tests/app/main/views/test_returned_letters.py | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/app/templates/views/returned-letters.html b/app/templates/views/returned-letters.html
index fe0e85462..f033a33bc 100644
--- a/app/templates/views/returned-letters.html
+++ b/app/templates/views/returned-letters.html
@@ -42,7 +42,7 @@
{% call field(align='right') %}
- Originally sent {{ item.created_at|format_date_normal }}
+ Sent {{ item.created_at|format_date_normal }}
{% endcall %}
diff --git a/tests/app/main/views/test_returned_letters.py b/tests/app/main/views/test_returned_letters.py
index 89b944e03..c8aadafc5 100644
--- a/tests/app/main/views/test_returned_letters.py
+++ b/tests/app/main/views/test_returned_letters.py
@@ -88,12 +88,12 @@ def test_returned_letters_page(
assert [
'Template name Originally sent',
- 'Example template Reference ABC123 Originally sent 24 December 2019',
- 'Example template Sent from Example spreadsheet.xlsx Originally sent 24 December 2019',
- 'Example template No reference provided Originally sent 24 December 2019',
- 'Example precompiled.pdf Reference DEF456 Originally sent 24 December 2019',
- 'Example one-off.pdf No reference provided Originally sent 24 December 2019',
- 'Provided as PDF Reference XYZ999 Originally sent 24 December 2019',
+ 'Example template Reference ABC123 Sent 24 December 2019',
+ 'Example template Sent from Example spreadsheet.xlsx Sent 24 December 2019',
+ 'Example template No reference provided Sent 24 December 2019',
+ 'Example precompiled.pdf Reference DEF456 Sent 24 December 2019',
+ 'Example one-off.pdf No reference provided Sent 24 December 2019',
+ 'Provided as PDF Reference XYZ999 Sent 24 December 2019',
] == [
normalize_spaces(row.text) for row in page.select('tr')
]
From 1dcbf7755a0f8b99c8fdc832b4568767fb21b4fa Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Thu, 20 Feb 2020 12:12:53 +0000
Subject: [PATCH 12/18] Add a back link to the received messages page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
It’s not a top level page (sits within the dashboard) so it should link
back to its parent page.
---
app/templates/views/dashboard/inbox.html | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/app/templates/views/dashboard/inbox.html b/app/templates/views/dashboard/inbox.html
index 39189c3de..8ea050b0b 100644
--- a/app/templates/views/dashboard/inbox.html
+++ b/app/templates/views/dashboard/inbox.html
@@ -1,4 +1,5 @@
{% from "components/ajax-block.html" import ajax_block %}
+{% from "components/page-header.html" import page_header %}
{% extends "withnav_template.html" %}
@@ -8,9 +9,10 @@
{% block maincolumn_content %}
-
- Received text messages
-
+ {{ page_header(
+ 'Received text messages',
+ back_link=url_for('main.service_dashboard', service_id=current_service.id)
+ ) }}
{{ ajax_block(
partials,
From 75927a8d05ac8028b9411a3d27c74d16dc909a5c Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Thu, 20 Feb 2020 15:11:10 +0000
Subject: [PATCH 13/18] Fix links to current service pages from guidance
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This wasn’t working because:
- macros don’t have access to `current_service` from within their scope
- there were no tests for this behaviour
---
...nt-service-link.html => service-link.html} | 6 ++--
.../guidance/branding-and-customisation.html | 10 +++----
.../guidance/create-and-send-messages.html | 4 +--
.../guidance/edit-and-format-messages.html | 8 ++---
.../views/guidance/upload-a-letter.html | 4 +--
tests/app/main/views/test_index.py | 30 ++++++++++++++++++-
6 files changed, 45 insertions(+), 17 deletions(-)
rename app/templates/components/{current-service-link.html => service-link.html} (58%)
diff --git a/app/templates/components/current-service-link.html b/app/templates/components/service-link.html
similarity index 58%
rename from app/templates/components/current-service-link.html
rename to app/templates/components/service-link.html
index 807ba8ac4..c4f128877 100644
--- a/app/templates/components/current-service-link.html
+++ b/app/templates/components/service-link.html
@@ -1,6 +1,6 @@
-{% macro current_service_link(endpoint, link_text) %}
- {% if current_service %}
- {{ link_text }}
+{% macro service_link(service, endpoint, link_text) %}
+ {% if service %}
+ {{ link_text }}
{% else %}
{{ link_text|capitalize }}
{% endif %}
diff --git a/app/templates/views/guidance/branding-and-customisation.html b/app/templates/views/guidance/branding-and-customisation.html
index cc68bcb75..44d8c1072 100644
--- a/app/templates/views/guidance/branding-and-customisation.html
+++ b/app/templates/views/guidance/branding-and-customisation.html
@@ -1,5 +1,5 @@
{% extends "content_template.html" %}
-{% from "components/current-service-link.html" import current_service_link %}
+{% from "components/service-link.html" import service_link %}
{% block per_page_title %}
Branding and customisation
@@ -25,7 +25,7 @@
To change your email branding:
- - Go to the Email settings section of the {{ current_service_link('main.service_settings', 'settings') }} page.
+ - Go to the Email settings section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.
- Select Change on the Email branding row.
@@ -36,7 +36,7 @@
To add a reply-to email address:
- - Go to the Email settings section of the {{ current_service_link('main.service_settings', 'settings') }} page.
+ - Go to the Email settings section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.
- Select Manage on the Reply-to email addresses row.
- Select Add reply-to address.
@@ -48,7 +48,7 @@
To change the text message sender from the default of ‘GOVUK’:
- - Go to the Text message settings section of the {{ current_service_link('main.service_settings', 'settings') }} page.
+ - Go to the Text message settings section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.
- Select Manage on the Text message senders row.
- Select Change or Add text message sender.
@@ -60,7 +60,7 @@
To add a logo to your letters:
- - Go to the Letter settings section of the {{ current_service_link('main.service_settings', 'settings') }} page.
+ - Go to the Letter settings section of the {{ service_link(current_service, 'main.service_settings', 'settings') }} page.
- Select Change on the Letter branding row.
diff --git a/app/templates/views/guidance/create-and-send-messages.html b/app/templates/views/guidance/create-and-send-messages.html
index fa905b33d..5f94387d7 100644
--- a/app/templates/views/guidance/create-and-send-messages.html
+++ b/app/templates/views/guidance/create-and-send-messages.html
@@ -1,5 +1,5 @@
{% extends "content_template.html" %}
-{% from "components/current-service-link.html" import current_service_link %}
+{% from "components/service-link.html" import service_link %}
{% block per_page_title %}
Send messages
@@ -15,7 +15,7 @@
To send a batch of messages at once:
- - Go to the {{ current_service_link('main.choose_template', 'templates') }} page and choose an existing template.
+ - Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page and choose an existing template.
- Select Send.
- If you’re sending emails, select Upload a list of email addresses. If you’re sending text messages, select Upload a list of phone numbers. If you’re sending letters, select Upload a list of addresses.
diff --git a/app/templates/views/guidance/edit-and-format-messages.html b/app/templates/views/guidance/edit-and-format-messages.html
index a80655a81..eb74e2e7d 100644
--- a/app/templates/views/guidance/edit-and-format-messages.html
+++ b/app/templates/views/guidance/edit-and-format-messages.html
@@ -1,5 +1,5 @@
{% extends "content_template.html" %}
-{% from "components/current-service-link.html" import current_service_link %}
+{% from "components/service-link.html" import service_link %}
{% block per_page_title %}
Edit and format messages
@@ -22,7 +22,7 @@
You can see a list of formatting instructions on the edit template page:
- - Go to the {{ current_service_link('main.choose_template', 'templates') }} page.
+ - Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.
- Add a new template or choose an existing template and select Edit.
@@ -61,7 +61,7 @@
To add a placeholder to the template:
- - Go to the {{ current_service_link('main.choose_template', 'templates') }} page.
+ - Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.
- Add a new template or choose an existing template and select Edit.
- Add a placeholder using double brackets. For example: Hello ((first name)), your reference is ((ref number)).
- Select Save.
@@ -81,7 +81,7 @@
To add optional content to your emails and text messages:
- - Go to the {{ current_service_link('main.choose_template', 'templates') }} page.
+ - Go to the {{ service_link(current_service, 'main.choose_template', 'templates') }} page.
- Add a new template or choose an existing template and select Edit.
- Use double brackets and ?? to define optional content. For example, if you only want to show something to people who are under 18: ((under18??Please get your application signed by a parent or guardian.))
- Select Save.
diff --git a/app/templates/views/guidance/upload-a-letter.html b/app/templates/views/guidance/upload-a-letter.html
index 42f33d765..619b2a5c3 100644
--- a/app/templates/views/guidance/upload-a-letter.html
+++ b/app/templates/views/guidance/upload-a-letter.html
@@ -1,5 +1,5 @@
{% extends "content_template.html" %}
-{% from "components/current-service-link.html" import current_service_link %}
+{% from "components/service-link.html" import service_link %}
{% block per_page_title %}
Upload a letter
@@ -14,7 +14,7 @@
To upload and send a letter from a PDF file:
- - Go to the {{ current_service_link('main.uploads', 'uploads') }} page.
+ - Go to the {{ service_link(current_service, 'main.uploads', 'uploads') }} page.
- Select Upload a letter.
- Select Choose file.
diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py
index 3b0de3657..bd4cea5a4 100644
--- a/tests/app/main/views/test_index.py
+++ b/tests/app/main/views/test_index.py
@@ -5,7 +5,7 @@ from bs4 import BeautifulSoup
from flask import url_for
from app.main.forms import FieldWithNoneOption
-from tests.conftest import normalize_spaces, sample_uuid
+from tests.conftest import SERVICE_ONE_ID, normalize_spaces, sample_uuid
def test_non_logged_in_user_can_see_homepage(
@@ -109,6 +109,34 @@ def test_static_pages(
request()
+def test_guidance_pages_link_to_service_pages_when_signed_in(
+ client_request,
+):
+ request = partial(client_request.get, 'main.edit_and_format_messages')
+ selector = '.list-number li a'
+
+ # Check the page loads when user is signed in
+ page = request()
+ assert page.select_one(selector)['href'] == url_for(
+ 'main.choose_template',
+ service_id=SERVICE_ONE_ID,
+ )
+
+ # Check it still works when they don’t have a recent service
+ with client_request.session_transaction() as session:
+ session['service_id'] = None
+ page = request()
+ assert not page.select_one(selector)
+
+ # Check it still works when they sign out
+ client_request.logout()
+ with client_request.session_transaction() as session:
+ session['service_id'] = None
+ session['user_id'] = None
+ page = request()
+ assert not page.select_one(selector)
+
+
@pytest.mark.parametrize('view, expected_view', [
('information_risk_management', 'security'),
('old_integration_testing', 'integration_testing'),
From f64f5725d16cea8ca3546941e5a36ee2b2709ceb Mon Sep 17 00:00:00 2001
From: Leo Hemsted
Date: Thu, 20 Feb 2020 17:48:48 +0000
Subject: [PATCH 14/18] Treat 400s from the api as internal server errors
if we expect a 400 (for example, the api returns 400 if the service name
is already in use) then we should handle that from the view function so
we can correctly display a relevation error message to the user. But if
it returns a 400 that we didn't expect (for example, because we sent
variables of the wrong type through to an endpoint with a schema), then
that is a bug that we should fix as any other raised exception. By
treating it as a 500 we encourage users to report it to us, and also
will get an alert email
---
app/__init__.py | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/app/__init__.py b/app/__init__.py
index c2921bafc..10c82a8a1 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -614,15 +614,10 @@ def register_errorhandlers(application): # noqa (C901 too complex)
error.message
))
error_code = error.status_code
- if error_code == 400:
- if isinstance(error.message, str):
- msg = [error.message]
- else:
- msg = list(itertools.chain(*[error.message[x] for x in error.message.keys()]))
- resp = make_response(render_template("error/400.html", message=msg))
- return useful_headers_after_request(resp)
- elif error_code not in [401, 404, 403, 410]:
- # probably a 500 or 503
+ if error_code not in [401, 404, 403, 410]:
+ # probably a 500 or 503.
+ # it might be a 400, which we should handle as if it's an internal server error. If the API might
+ # legitimately return a 400, we should handle that within the view or the client that calls it.
application.logger.exception("API {} failed with status {} message {}".format(
error.response.url if error.response else 'unknown',
error.status_code,
From da4ff4b448ecf509124865697dd83698ce93f343 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Fri, 21 Feb 2020 09:29:37 +0000
Subject: [PATCH 15/18] Add Design System class to remove visited state from
links
Co-Authored-By: Tom Byers
---
app/templates/components/service-link.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/templates/components/service-link.html b/app/templates/components/service-link.html
index c4f128877..b5906cf5f 100644
--- a/app/templates/components/service-link.html
+++ b/app/templates/components/service-link.html
@@ -1,6 +1,6 @@
{% macro service_link(service, endpoint, link_text) %}
{% if service %}
- {{ link_text }}
+ {{ link_text }}
{% else %}
{{ link_text|capitalize }}
{% endif %}
From a5b7e3b93aa1af24d5213ad13a88de4f724514a3 Mon Sep 17 00:00:00 2001
From: Chris Hill-Scott
Date: Fri, 21 Feb 2020 11:37:01 +0000
Subject: [PATCH 16/18] Add Design System link classes
Co-Authored-By: Tom Byers
---
app/templates/views/dashboard/_inbox.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/templates/views/dashboard/_inbox.html b/app/templates/views/dashboard/_inbox.html
index ebcd6fb84..a73ee296c 100644
--- a/app/templates/views/dashboard/_inbox.html
+++ b/app/templates/views/dashboard/_inbox.html
@@ -18,7 +18,7 @@
{% endif %}
{% if current_service.returned_letter_summary %}
-
+
{{ current_service.count_of_returned_letters_in_last_7_days|format_thousands }}
From d83250c7c2608d2a6f1d4e0dd5d983d46ac430e3 Mon Sep 17 00:00:00 2001
From: Leo Hemsted
Date: Thu, 20 Feb 2020 17:46:48 +0000
Subject: [PATCH 17/18] remove the generic 400 error page handler
it just shows a h1, so isn't helpful for people. We can re-use the 500
error page, which includes instructings "Try again later" and
instructions on what to do next (check the status page, email notify
support).
this required refactoring to ensure we can show the 500 error page while
still returning the required status code
---
app/__init__.py | 27 +++++++++++----------------
app/main/views/uploads.py | 1 -
app/templates/error/400.html | 11 -----------
tests/app/main/test_errorhandlers.py | 8 ++++----
tests/app/main/views/test_uploads.py | 1 +
5 files changed, 16 insertions(+), 32 deletions(-)
delete mode 100644 app/templates/error/400.html
diff --git a/app/__init__.py b/app/__init__.py
index 10c82a8a1..11e7f72c0 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,4 +1,3 @@
-import itertools
import os
import urllib
from datetime import datetime, timedelta, timezone
@@ -602,8 +601,10 @@ def useful_headers_after_request(response):
def register_errorhandlers(application): # noqa (C901 too complex)
- def _error_response(error_code):
- resp = make_response(render_template("error/{0}.html".format(error_code)), error_code)
+ def _error_response(error_code, error_page_template=None):
+ if error_page_template is None:
+ error_page_template = error_code
+ resp = make_response(render_template("error/{0}.html".format(error_page_template)), error_code)
return useful_headers_after_request(resp)
@application.errorhandler(HTTPError)
@@ -627,8 +628,10 @@ def register_errorhandlers(application): # noqa (C901 too complex)
return _error_response(error_code)
@application.errorhandler(400)
- def handle_400(error):
- return _error_response(400)
+ def handle_client_error(error):
+ # This is tripped if we call `abort(400)`.
+ application.logger.exception('Unhandled 400 client error')
+ return _error_response(400, error_page_template=500)
@application.errorhandler(410)
def handle_gone(error):
@@ -671,19 +674,11 @@ def register_errorhandlers(application): # noqa (C901 too complex)
u'csrf.invalid_token: Aborting request, user_id: {user_id}',
extra={'user_id': session['user_id']})
- resp = make_response(render_template(
- "error/400.html",
- message=['Something went wrong, please go back and try again.']
- ), 400)
- return useful_headers_after_request(resp)
+ return _error_response(400, error_page_template=500)
@application.errorhandler(405)
- def handle_405(error):
- resp = make_response(render_template(
- "error/400.html",
- message=['Something went wrong, please go back and try again.']
- ), 405)
- return useful_headers_after_request(resp)
+ def handle_method_not_allowed(error):
+ return _error_response(405, error_page_template=500)
@application.errorhandler(WerkzeugHTTPException)
def handle_http_error(error):
diff --git a/app/main/views/uploads.py b/app/main/views/uploads.py
index 60c28c894..332b6f9aa 100644
--- a/app/main/views/uploads.py
+++ b/app/main/views/uploads.py
@@ -234,7 +234,6 @@ def uploaded_letter_preview(service_id, file_id):
@main.route("/services//preview-letter-image/")
@user_has_permissions('send_messages')
def view_letter_upload_as_preview(service_id, file_id):
-
try:
page = int(request.args.get('page'))
except ValueError:
diff --git a/app/templates/error/400.html b/app/templates/error/400.html
deleted file mode 100644
index f874031c1..000000000
--- a/app/templates/error/400.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{% extends "withoutnav_template.html" %}
-{% block per_page_title %}Bad request{% endblock %}
-{% block maincolumn_content %}
-
-
-
- {{ message|join(",")}}
-
-
-
-{% endblock %}
diff --git a/tests/app/main/test_errorhandlers.py b/tests/app/main/test_errorhandlers.py
index 6c52612bb..eeb6bac00 100644
--- a/tests/app/main/test_errorhandlers.py
+++ b/tests/app/main/test_errorhandlers.py
@@ -51,8 +51,8 @@ def test_csrf_returns_400(logged_in_client, mocker):
assert response.status_code == 400
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
- assert page.h1.string.strip() == 'Something went wrong, please go back and try again.'
- assert page.title.string.strip() == 'Bad request – GOV.UK Notify'
+ assert page.h1.string.strip() == 'Sorry, there’s a problem with GOV.UK Notify'
+ assert page.title.string.strip() == 'Sorry, there’s a problem with the service – GOV.UK Notify'
def test_csrf_redirects_to_sign_in_page_if_not_signed_in(client, mocker):
@@ -70,5 +70,5 @@ def test_405_returns_something_went_wrong_page(client, mocker):
assert response.status_code == 405
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
- assert page.h1.string.strip() == 'Something went wrong, please go back and try again.'
- assert page.title.string.strip() == 'Bad request – GOV.UK Notify'
+ assert page.h1.string.strip() == 'Sorry, there’s a problem with GOV.UK Notify'
+ assert page.title.string.strip() == 'Sorry, there’s a problem with the service – GOV.UK Notify'
diff --git a/tests/app/main/views/test_uploads.py b/tests/app/main/views/test_uploads.py
index b4d1c9804..081e2fe35 100644
--- a/tests/app/main/views/test_uploads.py
+++ b/tests/app/main/views/test_uploads.py
@@ -504,6 +504,7 @@ def test_uploaded_letter_preview_image_400s_for_bad_page_type(
file_id=fake_uuid,
service_id=SERVICE_ONE_ID,
page='foo',
+ _test_page_title=False,
_expected_status=400,
)
From 853b2b137c6cffb39de42bd9c00eaf67f4336755 Mon Sep 17 00:00:00 2001
From: karlchillmaid
Date: Thu, 20 Feb 2020 12:03:02 +0000
Subject: [PATCH 18/18] Remove links to templates and permissions
---
app/templates/views/get-started.html | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/templates/views/get-started.html b/app/templates/views/get-started.html
index 6b553053f..2fc4390c1 100644
--- a/app/templates/views/get-started.html
+++ b/app/templates/views/get-started.html
@@ -43,17 +43,17 @@
-
Write some messages
- Add message templates with examples of the content you plan to send. You can use our guidance to help you.
+ Add message templates with examples of the content you plan to send. You can use our guidance to help you.
-
Set up your service
{% if not current_user.is_authenticated or not current_service %}
Review your settings to add message branding, reply-to addresses and sender information.
- Add team members and check their permissions.
+ Add team members and check their permissions.
{% else %}
Review your settings to add message branding, reply-to addresses and sender information.
- Add team members and check their permissions.
+ Add team members and check their permissions.
{% endif %}