diff --git a/app/main/forms.py b/app/main/forms.py index 065b62a6d..ae85a7740 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -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 they’ve 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", diff --git a/app/main/views/broadcast.py b/app/main/views/broadcast.py index a808e0a29..83dab3b44 100644 --- a/app/main/views/broadcast.py +++ b/app/main/views/broadcast.py @@ -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//broadcast-tour/') @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', diff --git a/app/templates/views/broadcast/view-message.html b/app/templates/views/broadcast/view-message.html index b9de5bcf2..5a447b032 100644 --- a/app/templates/views/broadcast/view-message.html +++ b/app/templates/views/broadcast/view-message.html @@ -92,6 +92,17 @@ wants to broadcast {{ broadcast_message.template.name }} + {% if current_service.live %} + {{ form.confirm(param_extensions={ + 'formGroup': { + 'classes': 'govuk-!-margin-bottom-4' + } + }) }} + {% else %} +

+ No phones will get this alert. +

+ {% 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), diff --git a/tests/app/main/views/test_broadcast.py b/tests/app/main/views/test_broadcast.py index 804505afb..897766940 100644 --- a/tests/app/main/views/test_broadcast.py +++ b/tests/app/main/views/test_broadcast.py @@ -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 they’ve 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: