Merge pull request #3881 from alphagov/confirm-live-broadcast

Add a confirmation checkbox for live broadcasts
This commit is contained in:
Chris Hill-Scott
2021-05-19 09:19:45 +01:00
committed by GitHub
4 changed files with 230 additions and 42 deletions

View File

@@ -1,3 +1,4 @@
import math
import weakref
from datetime import datetime, timedelta
from itertools import chain
@@ -1288,6 +1289,52 @@ class NewBroadcastForm(StripWhitespaceForm):
return self.content.data == 'template'
class ConfirmBroadcastForm(StripWhitespaceForm):
def __init__(self, *args, service_is_live, channel, max_phones, **kwargs):
super().__init__(*args, **kwargs)
self.confirm.label.text = self.generate_label(channel, max_phones)
if service_is_live:
self.confirm.validators += (
DataRequired('You need to confirm that you understand'),
)
confirm = GovukCheckboxField("Confirm")
@staticmethod
def generate_label(channel, max_phones):
if channel == 'test':
return (
'I understand this will alert anyone who has switched '
'on the test channel'
)
if channel == 'severe':
return (
f'I understand this will alert {ConfirmBroadcastForm.format_number_generic(max_phones)} '
'of people'
)
if channel == 'government':
return (
f'I understand this will alert {ConfirmBroadcastForm.format_number_generic(max_phones)} '
'of people, even if theyve opted out'
)
@staticmethod
def format_number_generic(count):
for threshold, message in (
(1_000_000, 'millions'),
(100_000, 'hundreds of thousands'),
(10_000, 'tens of thousands'),
(1_000, 'thousands'),
(100, 'hundreds'),
(-math.inf, 'an unknown number')
):
if count >= threshold:
return message
class BaseTemplateForm(StripWhitespaceForm):
name = GovukTextInputField(
"Template name",

View File

@@ -14,6 +14,7 @@ from app.main.forms import (
BroadcastAreaForm,
BroadcastAreaFormWithSelectAll,
BroadcastTemplateForm,
ConfirmBroadcastForm,
NewBroadcastForm,
SearchByNameForm,
)
@@ -21,6 +22,15 @@ from app.models.broadcast_message import BroadcastMessage, BroadcastMessages
from app.utils import service_has_permission, user_has_permissions
def _get_back_link_from_view_broadcast_endpoint():
return {
'main.view_current_broadcast': '.broadcast_dashboard',
'main.view_previous_broadcast': '.broadcast_dashboard_previous',
'main.view_rejected_broadcast': '.broadcast_dashboard_rejected',
'main.approve_broadcast_message': '.broadcast_dashboard',
}[request.endpoint]
@main.route('/services/<uuid:service_id>/broadcast-tour/<int:step_index>')
@user_has_permissions()
@service_has_permission('broadcast')
@@ -388,19 +398,18 @@ def view_broadcast(service_id, broadcast_message_id):
broadcast_message_id=broadcast_message.id,
))
back_link_endpoint = {
'main.view_current_broadcast': '.broadcast_dashboard',
'main.view_previous_broadcast': '.broadcast_dashboard_previous',
'main.view_rejected_broadcast': '.broadcast_dashboard_rejected',
}[request.endpoint]
return render_template(
'views/broadcast/view-message.html',
broadcast_message=broadcast_message,
back_link=url_for(
back_link_endpoint,
_get_back_link_from_view_broadcast_endpoint(),
service_id=current_service.id,
),
form=ConfirmBroadcastForm(
service_is_live=current_service.live,
channel=current_service.broadcast_channel,
max_phones=broadcast_message.count_of_phones_likely,
),
)
@@ -414,6 +423,12 @@ def approve_broadcast_message(service_id, broadcast_message_id):
service_id=current_service.id,
)
form = ConfirmBroadcastForm(
service_is_live=current_service.live,
channel=current_service.broadcast_channel,
max_phones=broadcast_message.count_of_phones_likely,
)
if broadcast_message.status != 'pending-approval':
return redirect(url_for(
'.view_current_broadcast',
@@ -421,14 +436,25 @@ def approve_broadcast_message(service_id, broadcast_message_id):
broadcast_message_id=broadcast_message.id,
))
broadcast_message.approve_broadcast()
if current_service.trial_mode:
broadcast_message.approve_broadcast()
return redirect(url_for(
'.broadcast_tour',
service_id=current_service.id,
step_index=6,
))
elif form.validate_on_submit():
broadcast_message.approve_broadcast()
else:
return render_template(
'views/broadcast/view-message.html',
broadcast_message=broadcast_message,
back_link=url_for(
_get_back_link_from_view_broadcast_endpoint(),
service_id=current_service.id,
),
form=form,
)
return redirect(url_for(
'.view_current_broadcast',

View File

@@ -92,6 +92,17 @@
wants to broadcast
{{ broadcast_message.template.name }}
</h1>
{% if current_service.live %}
{{ form.confirm(param_extensions={
'formGroup': {
'classes': 'govuk-!-margin-bottom-4'
}
}) }}
{% else %}
<p class="govuk-body govuk-!-margin-bottom-3">
No phones will get this alert.
</p>
{% endif %}
{{ page_footer(
"Start broadcasting now",
delete_link=url_for('main.reject_broadcast_message', service_id=current_service.id, broadcast_message_id=broadcast_message.id),

View File

@@ -1710,8 +1710,10 @@ def test_view_pending_broadcast(
normalize_spaces(page.select_one('.banner').text)
) == (
'Test User wants to broadcast Example template '
'No phones will get this alert. '
'Start broadcasting now Reject this alert'
)
assert not page.select('.banner input[type=checkbox]')
form = page.select_one('form.banner')
assert form['method'] == 'post'
@@ -1764,6 +1766,7 @@ def test_view_pending_broadcast_without_template(
normalize_spaces(page.select_one('.banner').text)
) == (
'Test User wants to broadcast No template test '
'No phones will get this alert. '
'Start broadcasting now Reject this alert'
)
assert (
@@ -1807,6 +1810,7 @@ def test_view_pending_broadcast_from_api_call(
normalize_spaces(page.select_one('.banner').text)
) == (
'An API call wants to broadcast abc123 '
'No phones will get this alert. '
'Start broadcasting now Reject this alert'
)
assert (
@@ -1817,6 +1821,101 @@ def test_view_pending_broadcast_from_api_call(
)
@pytest.mark.parametrize('channel, expected_label_text', (
('test', (
'I understand this will alert anyone who has switched on the test channel'
)),
('severe', (
'I understand this will alert millions of people'
)),
('government', (
'I understand this will alert millions of people, even if theyve opted out'
)),
))
@freeze_time('2020-02-22T22:22:22.000000')
def test_checkbox_to_confirm_non_training_broadcasts(
mocker,
client_request,
service_one,
active_user_with_permissions,
fake_uuid,
channel,
expected_label_text,
):
mocker.patch(
'app.broadcast_message_api_client.get_broadcast_message',
return_value=broadcast_message_json(
id_=fake_uuid,
service_id=SERVICE_ONE_ID,
template_id=None,
created_by_id=None,
status='pending-approval',
),
)
service_one['permissions'] += ['broadcast']
service_one['restricted'] = False
service_one['allowed_broadcast_provider'] = 'all'
service_one['broadcast_channel'] = channel
page = client_request.get(
'.view_current_broadcast',
service_id=SERVICE_ONE_ID,
broadcast_message_id=fake_uuid,
)
label = page.select_one('form.banner label')
assert label['for'] == 'confirm'
assert (
normalize_spaces(page.select_one('form.banner label').text)
) == expected_label_text
assert page.select_one('form.banner input[type=checkbox]')['name'] == 'confirm'
assert page.select_one('form.banner input[type=checkbox]')['value'] == 'y'
@freeze_time('2020-02-22T22:22:22.000000')
def test_confirm_approve_non_training_broadcasts_errors_if_not_ticked(
mocker,
client_request,
service_one,
active_user_with_permissions,
fake_uuid,
mock_update_broadcast_message,
mock_update_broadcast_message_status,
):
mocker.patch(
'app.broadcast_message_api_client.get_broadcast_message',
return_value=broadcast_message_json(
id_=fake_uuid,
service_id=SERVICE_ONE_ID,
template_id=None,
created_by_id=None,
status='pending-approval',
),
)
service_one['permissions'] += ['broadcast']
service_one['restricted'] = False
service_one['allowed_broadcast_provider'] = 'all'
service_one['broadcast_channel'] = 'severe'
page = client_request.post(
'.view_current_broadcast',
service_id=SERVICE_ONE_ID,
broadcast_message_id=fake_uuid,
_data={},
_expected_status=200,
)
error_message = page.select_one('form.banner .govuk-error-message')
assert error_message['id'] == 'confirm-error'
assert normalize_spaces(error_message.text) == (
'Error: You need to confirm that you understand'
)
assert mock_update_broadcast_message.called is False
assert mock_update_broadcast_message_status.called is False
@freeze_time('2020-02-22T22:22:22.000000')
def test_cant_approve_own_broadcast(
mocker,
@@ -1997,38 +2096,41 @@ def test_view_only_user_cant_approve_broadcast(
assert not page.select_one('.banner a')
@pytest.mark.parametrize('trial_mode, initial_status, expected_approval, expected_redirect', (
(True, 'draft', False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'pending-approval', True, partial(
url_for,
'.broadcast_tour',
step_index=6,
)),
(False, 'pending-approval', True, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'rejected', False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'broadcasting', False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'cancelled', False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
))
@pytest.mark.parametrize(
'trial_mode, initial_status, post_data, expected_approval, expected_redirect',
(
(True, 'draft', {}, False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'pending-approval', {}, True, partial(
url_for,
'.broadcast_tour',
step_index=6,
)),
(False, 'pending-approval', {'confirm': 'y'}, True, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'rejected', {}, False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'broadcasting', {}, False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
(True, 'cancelled', {}, False, partial(
url_for,
'.view_current_broadcast',
broadcast_message_id=sample_uuid,
)),
)
)
@freeze_time('2020-02-22T22:22:22.000000')
def test_request_approval(
mocker,
@@ -2039,6 +2141,7 @@ def test_request_approval(
mock_update_broadcast_message,
mock_update_broadcast_message_status,
initial_status,
post_data,
expected_approval,
trial_mode,
expected_redirect,
@@ -2064,7 +2167,8 @@ def test_request_approval(
_expected_redirect=expected_redirect(
service_id=SERVICE_ONE_ID,
_external=True,
)
),
_data=post_data,
)
if expected_approval: