diff --git a/app/__init__.py b/app/__init__.py index bff65c3b1..24ff7a6d0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -338,6 +338,14 @@ def valid_phone_number(phone_number): return False +def format_notification_type(notification_type): + return { + 'email': 'Email', + 'sms': 'SMS', + 'letter': 'Letter' + }[notification_type] + + def format_notification_status(status, template_type): return { 'email': { @@ -640,6 +648,7 @@ def add_template_filters(application): format_datetime_relative, format_delta, format_notification_status, + format_notification_type, format_notification_status_as_time, format_notification_status_as_field_status, format_notification_status_as_url, diff --git a/app/main/forms.py b/app/main/forms.py index 826080376..a22a72823 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -1024,3 +1024,27 @@ class BrandingOptionsEmail(StripWhitespaceForm): DataRequired() ], ) + + +class ServiceDataRetentionForm(StripWhitespaceForm): + + notification_type = RadioField( + 'What notification type?', + choices=[ + ('email', 'Email'), + ('sms', 'SMS'), + ('letter', 'Letter'), + ], + validators=[DataRequired()], + ) + days_of_retention = IntegerField(label="Days of retention", + validators=[validators.NumberRange(min=3, max=90, + message="Must be between 3 and 90")], + ) + + +class ServiceDataRetentionEditForm(StripWhitespaceForm): + days_of_retention = IntegerField(label="Days of retention", + validators=[validators.NumberRange(min=3, max=90, + message="Must be between 3 and 90")], + ) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 525bb1857..782c7f3a0 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -36,6 +36,8 @@ from app.main.forms import ( RequestToGoLiveForm, ServiceBasicViewForm, ServiceContactLinkForm, + ServiceDataRetentionEditForm, + ServiceDataRetentionForm, ServiceEditInboundNumberForm, ServiceInboundNumberForm, ServiceLetterContactBlockForm, @@ -1010,15 +1012,47 @@ def branding_request(service_id): ) -@main.route("/services//set-data-retention", methods=['GET', 'POST']) +@main.route("/services//data-retention", methods=['GET']) @login_required @user_is_platform_admin -def set_data_retention(service_id): +def data_retention(service_id): results = service_api_client.get_service_data_retention(service_id) - print("DATA RETENTION: ", results[0]) + return render_template('views/service-settings/data-retention.html', + data_retention_settings=results) + + +@main.route("/services//data-retention/add", methods=['GET', 'POST']) +@login_required +@user_is_platform_admin +def add_data_retention(service_id): + form = ServiceDataRetentionForm() + if form.validate_on_submit(): + service_api_client.create_service_data_retention(service_id, + form.notification_type.data, + form.days_of_retention.data) + return redirect(url_for('.data_retention', service_id=service_id)) return render_template( - 'views/service-settings/set-data-retention.html', - data_retentions=results + 'views/service-settings/data-retention/add.html', + form=form + ) + + +@main.route("/services//data-retention//edit", methods=['GET', 'POST']) +@login_required +@user_is_platform_admin +def edit_data_retention(service_id, data_retention_id): + form = ServiceDataRetentionEditForm() + if request.method == 'GET': + data_retention_item = service_api_client.get_service_data_retention_by_id(service_id, data_retention_id) + if form.validate_on_submit(): + service_api_client.update_service_data_retention(service_id, data_retention_id, form.days_of_retention.data) + return redirect(url_for('.data_retention', service_id=service_id)) + return render_template( + 'views/service-settings/data-retention/edit.html', + form=form, + data_retention_id=data_retention_id, + notification_type=data_retention_item['notification_type'], + days_of_retention=data_retention_item['days_of_retention'] ) diff --git a/app/navigation.py b/app/navigation.py index 4860e4adf..fef43debe 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -101,6 +101,7 @@ class HeaderNavigation(Navigation): 'accept_invite', 'accept_org_invite', 'action_blocked', + 'add_data_retention', 'add_service', 'add_service_template', 'add_template_by_type', @@ -131,12 +132,14 @@ class HeaderNavigation(Navigation): 'conversation_updates', 'cookies', 'create_api_key', + 'data_retention', 'delete_service_template', 'delivery_and_failure', 'delivery_status_callback', 'design_content', 'download_agreement', 'download_notifications_csv', + 'edit_data_retention', 'edit_organisation_name', 'edit_provider', 'edit_service_template', @@ -366,6 +369,7 @@ class MainNavigation(Navigation): exclude = { 'accept_invite', 'accept_org_invite', + 'add_data_retention', 'add_organisation', 'add_service', 'agreement', @@ -385,11 +389,13 @@ class MainNavigation(Navigation): 'conversation_updates', 'cookies', 'create_email_branding', + 'data_retention', 'delivery_and_failure', 'design_content', 'documentation', 'download_agreement', 'download_notifications_csv', + 'edit_data_retention', 'edit_organisation_name', 'edit_provider', 'edit_user_org_permissions', @@ -517,6 +523,7 @@ class CaseworkNavigation(Navigation): 'accept_invite', 'accept_org_invite', 'action_blocked', + 'add_data_retention', 'add_organisation', 'add_service', 'add_service_template', @@ -549,6 +556,7 @@ class CaseworkNavigation(Navigation): 'cookies', 'create_api_key', 'create_email_branding', + 'data_retention', 'delete_service_template', 'delivery_and_failure', 'delivery_status_callback', @@ -556,6 +564,7 @@ class CaseworkNavigation(Navigation): 'documentation', 'download_agreement', 'download_notifications_csv', + 'edit_data_retention', 'edit_organisation_name', 'edit_provider', 'edit_service_template', @@ -741,6 +750,7 @@ class OrgNavigation(Navigation): 'accept_invite', 'accept_org_invite', 'action_blocked', + 'add_data_retention', 'add_organisation', 'add_service', 'add_service_template', @@ -773,6 +783,7 @@ class OrgNavigation(Navigation): 'cookies', 'create_api_key', 'create_email_branding', + 'data_retention', 'delete_service_template', 'delivery_and_failure', 'delivery_status_callback', @@ -780,6 +791,7 @@ class OrgNavigation(Navigation): 'documentation', 'download_agreement', 'download_notifications_csv', + 'edit_data_retention', 'edit_provider', 'edit_service_template', 'edit_user_permissions', diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 77d7799e9..2297fbfe2 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -322,8 +322,10 @@ {% endcall %} {% call row() %} {{ text_field('Data Retention')}} - {{ text_field(data_retention[0]['notification_type']) }} - {{ edit_field('Change', url_for('.set_data_retention', service_id=current_service.id)) }} + {% call field() %} + {{ data_retention | join(', ', attribute='notification_type') }} + {% endcall %} + {{ edit_field('Change', url_for('.data_retention', service_id=current_service.id)) }} {% endcall %} {% endcall %} diff --git a/app/templates/views/service-settings/data-retention.html b/app/templates/views/service-settings/data-retention.html new file mode 100644 index 000000000..67322dffc --- /dev/null +++ b/app/templates/views/service-settings/data-retention.html @@ -0,0 +1,44 @@ +{% extends "withnav_template.html" %} +{% from "components/api-key.html" import api_key %} +{% from "components/page-footer.html" import page_footer %} +{% from "components/table.html" import row_group, row, text_field, edit_field, field, boolean_field, list_table with context %} + +{% block service_page_title %} + Email reply to addresses +{% endblock %} + +{% block maincolumn_content %} + +
+
+

+ Data retention +

+
+ +
+
+
+ By default data is kept for 7 days +
+
+
+ {% if not data_retention_settings %} +
+ You haven’t added any data retention +
+ {% endif %} + {% for item in data_retention_settings %} +
+ +

+ {{ item.notification_type | format_notification_type }} notifications will be kept for {{ item.days_of_retention }} days + Change +

+
+ {% endfor %} +
+ +{% endblock %} diff --git a/app/templates/views/service-settings/data-retention/add.html b/app/templates/views/service-settings/data-retention/add.html new file mode 100644 index 000000000..da70d086f --- /dev/null +++ b/app/templates/views/service-settings/data-retention/add.html @@ -0,0 +1,25 @@ +{% extends "withnav_template.html" %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} + +{% from "components/radios.html" import radios %} + +{% block service_page_title %} + Data retention +{% endblock %} + +{% block maincolumn_content %} + + +

Set data retention

+
+ {{ radios(form.notification_type) }} + {{ textbox(form.days_of_retention) }} + {{ page_footer( + 'Add', + back_link=url_for('.add_data_retention', service_id=current_service.id), + back_link_text='Back' + ) }} +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/views/service-settings/data-retention/edit.html b/app/templates/views/service-settings/data-retention/edit.html new file mode 100644 index 000000000..21a7c8736 --- /dev/null +++ b/app/templates/views/service-settings/data-retention/edit.html @@ -0,0 +1,25 @@ +{% extends "withnav_template.html" %} +{% from "components/textbox.html" import textbox %} +{% from "components/page-footer.html" import page_footer %} + +{% from "components/radios.html" import radios %} + +{% block service_page_title %} + Data retention +{% endblock %} + +{% block maincolumn_content %} + + +

Set data retention

+
+ {{ notification_type | capitalize}} + {{ textbox(form.days_of_retention) }} + {{ page_footer( + 'Save', + back_link=url_for('.edit_data_retention', service_id=current_service.id, data_retention_id=data_retention_id), + back_link_text='Back' + ) }} +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/views/service-settings/set-data-retention.html b/app/templates/views/service-settings/set-data-retention.html deleted file mode 100644 index 42c318b67..000000000 --- a/app/templates/views/service-settings/set-data-retention.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "withnav_template.html" %} -{% from "components/textbox.html" import textbox %} -{% from "components/page-footer.html" import page_footer %} - -{% block service_page_title %} - Data retention -{% endblock %} - -{% block maincolumn_content %} - -
-
-

Emails

-

- Add new data retention here -

- {% for item in data_retentions %} - {{ item.notification_type }} - {{ item.days_of_retention }} - {% endfor %} - {{ page_footer( - back_link=url_for('.service_settings', service_id=current_service.id), - back_link_text='Back to settings' - ) }} -
-
- -{% endblock %} \ No newline at end of file diff --git a/tests/app/main/views/service_settings/test_service_setting_permissions.py b/tests/app/main/views/service_settings/test_service_setting_permissions.py index 40b3b85a7..51cd9d805 100644 --- a/tests/app/main/views/service_settings/test_service_setting_permissions.py +++ b/tests/app/main/views/service_settings/test_service_setting_permissions.py @@ -16,6 +16,7 @@ def get_service_settings_page( no_reply_to_email_addresses, no_letter_contact_blocks, single_sms_sender, + mock_get_service_data_retention, ): client_request.login(platform_admin_user) return functools.partial(client_request.get, 'main.service_settings', service_id=service_one['id']) @@ -103,6 +104,7 @@ def test_normal_user_doesnt_see_any_toggle_buttons( mock_get_letter_email_branding, mock_get_inbound_number_for_service, mock_get_free_sms_fragment_limit, + mock_get_service_data_retention ): page = client_request.get('main.service_settings', service_id=service_one['id']) toggles = page.find('a', {'class': 'button'}) diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index dcb37e915..3d0c6603c 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -40,6 +40,7 @@ def mock_get_service_settings_page_common( mock_get_letter_email_branding, mock_get_inbound_number_for_service, mock_get_free_sms_fragment_limit, + mock_get_service_data_retention, ): return @@ -94,6 +95,7 @@ def mock_get_service_settings_page_common( 'Free text message allowance 250,000 Change', 'Email branding GOV.UK Change', 'Letter branding HM Government Change', + 'Data Retention Change' ]), ]) @@ -2527,6 +2529,7 @@ def test_service_settings_when_inbound_number_is_not_set( mocker, mock_get_letter_email_branding, mock_get_free_sms_fragment_limit, + mock_get_service_data_retention, ): mocker.patch('app.inbound_number_client.get_inbound_sms_number_for_service', return_value={'data': {}}) diff --git a/tests/conftest.py b/tests/conftest.py index f9cb681c5..d02db1a6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2670,6 +2670,12 @@ def normalize_spaces(input): return normalize_spaces(' '.join(item.text for item in input)) +@pytest.fixture(scope='function') +def mock_get_service_data_retention(mocker): + return mocker.patch('app.service_api_client.get_service_data_retention', + return_value={}) + + @pytest.fixture(scope='function') def mock_get_free_sms_fragment_limit(mocker): sample_limit = 250000