mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-08-20 14:29:51 -04:00
Merge pull request #3881 from alphagov/confirm-live-broadcast
Add a confirmation checkbox for live broadcasts
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import math
|
||||||
import weakref
|
import weakref
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
@@ -1288,6 +1289,52 @@ class NewBroadcastForm(StripWhitespaceForm):
|
|||||||
return self.content.data == 'template'
|
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):
|
class BaseTemplateForm(StripWhitespaceForm):
|
||||||
name = GovukTextInputField(
|
name = GovukTextInputField(
|
||||||
"Template name",
|
"Template name",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from app.main.forms import (
|
|||||||
BroadcastAreaForm,
|
BroadcastAreaForm,
|
||||||
BroadcastAreaFormWithSelectAll,
|
BroadcastAreaFormWithSelectAll,
|
||||||
BroadcastTemplateForm,
|
BroadcastTemplateForm,
|
||||||
|
ConfirmBroadcastForm,
|
||||||
NewBroadcastForm,
|
NewBroadcastForm,
|
||||||
SearchByNameForm,
|
SearchByNameForm,
|
||||||
)
|
)
|
||||||
@@ -21,6 +22,15 @@ from app.models.broadcast_message import BroadcastMessage, BroadcastMessages
|
|||||||
from app.utils import service_has_permission, user_has_permissions
|
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>')
|
@main.route('/services/<uuid:service_id>/broadcast-tour/<int:step_index>')
|
||||||
@user_has_permissions()
|
@user_has_permissions()
|
||||||
@service_has_permission('broadcast')
|
@service_has_permission('broadcast')
|
||||||
@@ -388,19 +398,18 @@ def view_broadcast(service_id, broadcast_message_id):
|
|||||||
broadcast_message_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(
|
return render_template(
|
||||||
'views/broadcast/view-message.html',
|
'views/broadcast/view-message.html',
|
||||||
broadcast_message=broadcast_message,
|
broadcast_message=broadcast_message,
|
||||||
back_link=url_for(
|
back_link=url_for(
|
||||||
back_link_endpoint,
|
_get_back_link_from_view_broadcast_endpoint(),
|
||||||
service_id=current_service.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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -414,6 +423,12 @@ def approve_broadcast_message(service_id, broadcast_message_id):
|
|||||||
service_id=current_service.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':
|
if broadcast_message.status != 'pending-approval':
|
||||||
return redirect(url_for(
|
return redirect(url_for(
|
||||||
'.view_current_broadcast',
|
'.view_current_broadcast',
|
||||||
@@ -421,14 +436,25 @@ def approve_broadcast_message(service_id, broadcast_message_id):
|
|||||||
broadcast_message_id=broadcast_message.id,
|
broadcast_message_id=broadcast_message.id,
|
||||||
))
|
))
|
||||||
|
|
||||||
broadcast_message.approve_broadcast()
|
|
||||||
|
|
||||||
if current_service.trial_mode:
|
if current_service.trial_mode:
|
||||||
|
broadcast_message.approve_broadcast()
|
||||||
return redirect(url_for(
|
return redirect(url_for(
|
||||||
'.broadcast_tour',
|
'.broadcast_tour',
|
||||||
service_id=current_service.id,
|
service_id=current_service.id,
|
||||||
step_index=6,
|
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(
|
return redirect(url_for(
|
||||||
'.view_current_broadcast',
|
'.view_current_broadcast',
|
||||||
|
|||||||
@@ -92,6 +92,17 @@
|
|||||||
wants to broadcast
|
wants to broadcast
|
||||||
{{ broadcast_message.template.name }}
|
{{ broadcast_message.template.name }}
|
||||||
</h1>
|
</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(
|
{{ page_footer(
|
||||||
"Start broadcasting now",
|
"Start broadcasting now",
|
||||||
delete_link=url_for('main.reject_broadcast_message', service_id=current_service.id, broadcast_message_id=broadcast_message.id),
|
delete_link=url_for('main.reject_broadcast_message', service_id=current_service.id, broadcast_message_id=broadcast_message.id),
|
||||||
|
|||||||
@@ -1710,8 +1710,10 @@ def test_view_pending_broadcast(
|
|||||||
normalize_spaces(page.select_one('.banner').text)
|
normalize_spaces(page.select_one('.banner').text)
|
||||||
) == (
|
) == (
|
||||||
'Test User wants to broadcast Example template '
|
'Test User wants to broadcast Example template '
|
||||||
|
'No phones will get this alert. '
|
||||||
'Start broadcasting now Reject this alert'
|
'Start broadcasting now Reject this alert'
|
||||||
)
|
)
|
||||||
|
assert not page.select('.banner input[type=checkbox]')
|
||||||
|
|
||||||
form = page.select_one('form.banner')
|
form = page.select_one('form.banner')
|
||||||
assert form['method'] == 'post'
|
assert form['method'] == 'post'
|
||||||
@@ -1764,6 +1766,7 @@ def test_view_pending_broadcast_without_template(
|
|||||||
normalize_spaces(page.select_one('.banner').text)
|
normalize_spaces(page.select_one('.banner').text)
|
||||||
) == (
|
) == (
|
||||||
'Test User wants to broadcast No template test '
|
'Test User wants to broadcast No template test '
|
||||||
|
'No phones will get this alert. '
|
||||||
'Start broadcasting now Reject this alert'
|
'Start broadcasting now Reject this alert'
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
@@ -1807,6 +1810,7 @@ def test_view_pending_broadcast_from_api_call(
|
|||||||
normalize_spaces(page.select_one('.banner').text)
|
normalize_spaces(page.select_one('.banner').text)
|
||||||
) == (
|
) == (
|
||||||
'An API call wants to broadcast abc123 '
|
'An API call wants to broadcast abc123 '
|
||||||
|
'No phones will get this alert. '
|
||||||
'Start broadcasting now Reject this alert'
|
'Start broadcasting now Reject this alert'
|
||||||
)
|
)
|
||||||
assert (
|
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')
|
@freeze_time('2020-02-22T22:22:22.000000')
|
||||||
def test_cant_approve_own_broadcast(
|
def test_cant_approve_own_broadcast(
|
||||||
mocker,
|
mocker,
|
||||||
@@ -1997,38 +2096,41 @@ def test_view_only_user_cant_approve_broadcast(
|
|||||||
assert not page.select_one('.banner a')
|
assert not page.select_one('.banner a')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('trial_mode, initial_status, expected_approval, expected_redirect', (
|
@pytest.mark.parametrize(
|
||||||
(True, 'draft', False, partial(
|
'trial_mode, initial_status, post_data, expected_approval, expected_redirect',
|
||||||
url_for,
|
(
|
||||||
'.view_current_broadcast',
|
(True, 'draft', {}, False, partial(
|
||||||
broadcast_message_id=sample_uuid,
|
url_for,
|
||||||
)),
|
'.view_current_broadcast',
|
||||||
(True, 'pending-approval', True, partial(
|
broadcast_message_id=sample_uuid,
|
||||||
url_for,
|
)),
|
||||||
'.broadcast_tour',
|
(True, 'pending-approval', {}, True, partial(
|
||||||
step_index=6,
|
url_for,
|
||||||
)),
|
'.broadcast_tour',
|
||||||
(False, 'pending-approval', True, partial(
|
step_index=6,
|
||||||
url_for,
|
)),
|
||||||
'.view_current_broadcast',
|
(False, 'pending-approval', {'confirm': 'y'}, True, partial(
|
||||||
broadcast_message_id=sample_uuid,
|
url_for,
|
||||||
)),
|
'.view_current_broadcast',
|
||||||
(True, 'rejected', False, partial(
|
broadcast_message_id=sample_uuid,
|
||||||
url_for,
|
)),
|
||||||
'.view_current_broadcast',
|
(True, 'rejected', {}, False, partial(
|
||||||
broadcast_message_id=sample_uuid,
|
url_for,
|
||||||
)),
|
'.view_current_broadcast',
|
||||||
(True, 'broadcasting', False, partial(
|
broadcast_message_id=sample_uuid,
|
||||||
url_for,
|
)),
|
||||||
'.view_current_broadcast',
|
(True, 'broadcasting', {}, False, partial(
|
||||||
broadcast_message_id=sample_uuid,
|
url_for,
|
||||||
)),
|
'.view_current_broadcast',
|
||||||
(True, 'cancelled', False, partial(
|
broadcast_message_id=sample_uuid,
|
||||||
url_for,
|
)),
|
||||||
'.view_current_broadcast',
|
(True, 'cancelled', {}, False, partial(
|
||||||
broadcast_message_id=sample_uuid,
|
url_for,
|
||||||
)),
|
'.view_current_broadcast',
|
||||||
))
|
broadcast_message_id=sample_uuid,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
)
|
||||||
@freeze_time('2020-02-22T22:22:22.000000')
|
@freeze_time('2020-02-22T22:22:22.000000')
|
||||||
def test_request_approval(
|
def test_request_approval(
|
||||||
mocker,
|
mocker,
|
||||||
@@ -2039,6 +2141,7 @@ def test_request_approval(
|
|||||||
mock_update_broadcast_message,
|
mock_update_broadcast_message,
|
||||||
mock_update_broadcast_message_status,
|
mock_update_broadcast_message_status,
|
||||||
initial_status,
|
initial_status,
|
||||||
|
post_data,
|
||||||
expected_approval,
|
expected_approval,
|
||||||
trial_mode,
|
trial_mode,
|
||||||
expected_redirect,
|
expected_redirect,
|
||||||
@@ -2064,7 +2167,8 @@ def test_request_approval(
|
|||||||
_expected_redirect=expected_redirect(
|
_expected_redirect=expected_redirect(
|
||||||
service_id=SERVICE_ONE_ID,
|
service_id=SERVICE_ONE_ID,
|
||||||
_external=True,
|
_external=True,
|
||||||
)
|
),
|
||||||
|
_data=post_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
if expected_approval:
|
if expected_approval:
|
||||||
|
|||||||
Reference in New Issue
Block a user