diff --git a/app/main/forms.py b/app/main/forms.py index f126ec7d0..59097037f 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1183,6 +1183,15 @@ class MessageLimit(StripWhitespaceForm): ) +class RateLimit(StripWhitespaceForm): + rate_limit = GovukIntegerField( + 'Number of messages the service can send in a rolling 60 second window', + validators=[ + DataRequired(message='Cannot be empty') + ] + ) + + class ConfirmPasswordForm(StripWhitespaceForm): def __init__(self, validate_password_func, *args, **kwargs): self.validate_password_func = validate_password_func diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 0430b99b8..9841327ab 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -38,6 +38,7 @@ from app.main.forms import ( LinkOrganisationsForm, MessageLimit, PreviewBranding, + RateLimit, RenameServiceForm, SearchByNameForm, ServiceContactDetailsForm, @@ -956,6 +957,23 @@ def set_message_limit(service_id): ) +@main.route("/services//service-settings/set-rate-limit", methods=['GET', 'POST']) +@user_is_platform_admin +def set_rate_limit(service_id): + + form = RateLimit(rate_limit=current_service.rate_limit) + + if form.validate_on_submit(): + current_service.update(rate_limit=form.rate_limit.data) + + return redirect(url_for('.service_settings', service_id=service_id)) + + return render_template( + 'views/service-settings/set-rate-limit.html', + form=form, + ) + + @main.route("/services//service-settings/set-email-branding", methods=['GET', 'POST']) @user_is_platform_admin def service_set_email_branding(service_id): diff --git a/app/models/service.py b/app/models/service.py index a88b580e6..c5350b297 100644 --- a/app/models/service.py +++ b/app/models/service.py @@ -37,6 +37,7 @@ class Service(JSONModel): 'inbound_api', 'letter_branding', 'message_limit', + 'rate_limit', 'name', 'prefix_sms', 'research_mode', diff --git a/app/navigation.py b/app/navigation.py index 652ca605c..4cb319cbc 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -317,6 +317,7 @@ class HeaderNavigation(Navigation): 'services_or_dashboard', 'set_free_sms_allowance', 'set_message_limit', + 'set_rate_limit', 'set_sender', 'set_template_sender', 'show_accounts_or_dashboard', @@ -505,6 +506,7 @@ class MainNavigation(Navigation): 'service_sms_senders', 'set_free_sms_allowance', 'set_message_limit', + 'set_rate_limit', 'service_set_letter_branding', 'submit_request_to_go_live', }, @@ -972,6 +974,7 @@ class CaseworkNavigation(Navigation): 'services_or_dashboard', 'set_free_sms_allowance', 'set_message_limit', + 'set_rate_limit', 'service_set_letter_branding', 'set_sender', 'set_template_sender', @@ -1285,6 +1288,7 @@ class OrgNavigation(Navigation): 'services_or_dashboard', 'set_free_sms_allowance', 'set_message_limit', + 'set_rate_limit', 'service_set_letter_branding', 'set_sender', 'set_template_sender', diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 7d5cc01a4..668939cc5 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -353,6 +353,11 @@ {% endcall %} {{ edit_field('Change', url_for('.link_service_to_organisation', service_id=current_service.id), suffix='organisation for service') }} {% endcall %} + {% call row() %} + {{ text_field('Rate limit')}} + {{ text_field('{:,} per minute'.format(current_service.rate_limit)) }} + {{ edit_field('Change', url_for('.set_rate_limit', service_id=current_service.id), suffix='rate limit') }} + {% endcall %} {% call row() %} {{ text_field('Message limit')}} {{ text_field('{:,} per day'.format(current_service.message_limit)) }} diff --git a/app/templates/views/service-settings/set-rate-limit.html b/app/templates/views/service-settings/set-rate-limit.html new file mode 100644 index 000000000..5274fb655 --- /dev/null +++ b/app/templates/views/service-settings/set-rate-limit.html @@ -0,0 +1,21 @@ +{% extends "withnav_template.html" %} +{% from "components/page-header.html" import page_header %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/form.html" import form_wrapper %} + +{% block service_page_title %} + Set rate limit +{% endblock %} + +{% block maincolumn_content %} + + {% call form_wrapper() %} + {{ page_header( + 'Rate limit', + back_link=url_for('.service_settings', service_id=current_service.id) + ) }} + {{ form.rate_limit }} + {{ page_footer('Save') }} + {% endcall %} + +{% endblock %} diff --git a/tests/__init__.py b/tests/__init__.py index 01604ce50..8b0ee34c9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -149,6 +149,7 @@ def service_json( prefix_sms=True, contact_link=None, organisation_id=None, + rate_limit=3000, ): if users is None: users = [] @@ -163,6 +164,7 @@ def service_json( 'name': name, 'users': users, 'message_limit': message_limit, + 'rate_limit': rate_limit, 'active': active, 'restricted': restricted, 'email_from': email_from, diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 521784f65..61511985d 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -101,6 +101,7 @@ def mock_get_service_settings_page_common( 'Live Off Change service status', 'Count in list of live services Yes Change if service is counted in list of live services', 'Organisation Test organisation Central government Change organisation for service', + 'Rate limit 3,000 per minute Change rate limit', 'Message limit 1,000 per day Change daily message limit', 'Free text message allowance 250,000 per year Change free text message allowance', 'Email branding GOV.UK Change email branding (admin view)', @@ -3579,6 +3580,7 @@ def test_should_set_branding_and_organisations( @pytest.mark.parametrize('endpoint', [ 'main.set_free_sms_allowance', 'main.set_message_limit', + 'main.set_rate_limit', ]) def test_organisation_type_pages_are_platform_admin_only( client_request, @@ -3658,6 +3660,28 @@ def test_should_show_page_to_set_message_limit( ) +def test_should_show_page_to_set_rate_limit( + platform_admin_client, +): + response = platform_admin_client.get(url_for( + 'main.set_rate_limit', + service_id=SERVICE_ONE_ID + )) + assert response.status_code == 200 + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + + assert normalize_spaces(page.select_one('label').text) == ( + 'Number of messages the service can send in a rolling 60 second window' + ) + assert normalize_spaces(page.select_one('input[type=text]')['value']) == ( + '3000' + ) + + +@pytest.mark.parametrize('endpoint, field_name', ( + ('main.set_message_limit', 'message_limit'), + ('main.set_rate_limit', 'rate_limit'), +)) @pytest.mark.parametrize('new_limit, expected_api_argument', [ ('1', 1), ('250000', 250000), @@ -3668,15 +3692,17 @@ def test_should_set_message_limit( new_limit, expected_api_argument, mock_update_service, + endpoint, + field_name, ): response = platform_admin_client.post( url_for( - 'main.set_message_limit', + endpoint, service_id=SERVICE_ONE_ID, ), data={ - 'message_limit': new_limit, + field_name: new_limit, }, ) assert response.status_code == 302 @@ -3684,7 +3710,7 @@ def test_should_set_message_limit( mock_update_service.assert_called_once_with( SERVICE_ONE_ID, - message_limit=expected_api_argument, + **{field_name: expected_api_argument}, )