diff --git a/app/main/forms.py b/app/main/forms.py index 537b11ba7..dd100c9b1 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -885,3 +885,17 @@ class SetTemplateSenderForm(StripWhitespaceForm): self.sender.label.text = 'Select your sender' sender = RadioField() + + +class LinkOrganisationsForm(StripWhitespaceForm): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.organisations.choices = kwargs['choices'] + + organisations = RadioField( + 'Select an organisation', + validators=[ + DataRequired() + ] + ) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index b3a2a81eb..20789498d 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -17,7 +17,6 @@ from notifications_utils.field import Field from notifications_utils.clients import DeskproError from notifications_python_client.errors import HTTPError -from app import service_api_client, deskpro_client from app.main import main from app.utils import user_has_permissions, email_safe, get_cdn_domain from app.main.forms import ( @@ -36,8 +35,18 @@ from app.main.forms import ( ServiceEditInboundNumberForm, SMSPrefixForm, ServiceSwitchLettersForm, + LinkOrganisationsForm, +) +from app import ( + service_api_client, + deskpro_client, + user_api_client, + current_service, + email_branding_client, + inbound_number_client, + billing_api_client, + organisations_client, ) -from app import user_api_client, current_service, email_branding_client, inbound_number_client, billing_api_client from notifications_utils.formatters import formatted_list @@ -46,6 +55,8 @@ from notifications_utils.formatters import formatted_list @user_has_permissions('manage_settings', 'manage_api_keys', admin_override=True, any_=True) def service_settings(service_id): letter_branding_organisations = email_branding_client.get_letter_email_branding() + organisation = organisations_client.get_service_organisation(service_id).get('name', None) + if current_service['email_branding']: email_branding = email_branding_client.get_email_branding(current_service['email_branding'])['email_branding'] else: @@ -87,6 +98,7 @@ def service_settings(service_id): sms_sender_count=sms_sender_count, free_sms_fragment_limit=free_sms_fragment_limit, prefix_sms=current_service['prefix_sms'], + organisation=organisation, ) @@ -762,6 +774,33 @@ def set_letter_branding(service_id): ) +@main.route("/services//service-settings/link-service-to-organisation", methods=['GET', 'POST']) +@login_required +@user_has_permissions(admin_override=True) +def link_service_to_organisation(service_id): + + organisations = organisations_client.get_organisations() + current_organisation = organisations_client.get_service_organisation(service_id).get('id', None) + + form = LinkOrganisationsForm( + choices=convert_dictionary_to_wtforms_choices_format(organisations, 'id', 'name'), + organisations=current_organisation + ) + + if form.validate_on_submit(): + if form.organisations.data != current_organisation: + organisations_client.update_service_organisation( + service_id, + form.organisations.data + ) + return redirect(url_for('.service_settings', service_id=service_id)) + + return render_template( + 'views/service-settings/link-service-to-organisation.html', + form=form, + ) + + def get_branding_as_value_and_label(email_branding): return [ (branding['id'], branding['name']) @@ -776,3 +815,9 @@ def get_branding_as_dict(email_branding): 'colour': branding['colour'] } for branding in email_branding } + + +def convert_dictionary_to_wtforms_choices_format(dictionary, value, label): + return [ + (item[value], item[label]) for item in dictionary + ] diff --git a/app/notify_client/organisations_api_client.py b/app/notify_client/organisations_api_client.py index ade5ad378..34b32f8c9 100644 --- a/app/notify_client/organisations_api_client.py +++ b/app/notify_client/organisations_api_client.py @@ -28,3 +28,15 @@ class OrganisationsClient(NotifyAdminAPIClient): "name": name } return self.post(url="/organisations/{}".format(org_id), data=data) + + def get_service_organisation(self, service_id): + return self.get(url="/service/{}/organisation".format(service_id)) + + def update_service_organisation(self, service_id, organisation_id): + data = { + 'service_id': service_id + } + return self.post( + url="/organisations/{}/service".format(organisation_id), + data=data + ) diff --git a/app/templates/views/organisations/manage-organisation.html b/app/templates/views/organisations/manage-organisation.html index b9d18cea7..41ab44e04 100644 --- a/app/templates/views/organisations/manage-organisation.html +++ b/app/templates/views/organisations/manage-organisation.html @@ -14,11 +14,12 @@

{{ '{} an organisation'.format('Update' if organisation else 'Create')}}

- {{textbox(form.name)}} - {{ page_footer( - 'Save', - back_link=url_for('.organisations'), - back_link_text='Back to organisations', - ) }} + {{textbox(form.name)}} + {{ page_footer( + 'Save', + back_link=url_for('.organisations'), + back_link_text='Back to organisations', + ) }}
+ {% endblock %} diff --git a/app/templates/views/organisations/organisations.html b/app/templates/views/organisations/organisations.html index 136e61d4a..dfc7af01e 100644 --- a/app/templates/views/organisations/organisations.html +++ b/app/templates/views/organisations/organisations.html @@ -15,7 +15,7 @@ Organisations
Create an organisation diff --git a/app/templates/views/organisations/view-organisation.html b/app/templates/views/organisations/view-organisation.html index bcb7ce474..2c7946bb2 100644 --- a/app/templates/views/organisations/view-organisation.html +++ b/app/templates/views/organisations/view-organisation.html @@ -12,12 +12,9 @@ {% block platform_admin_content %}

-
{{ organisation['name'] }}
+ {{ organisation['name'] }}

-
-
- [List of services] -
-
+

[List of services]

+ {% endblock %} diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 5057419b8..641f1f7fb 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -255,6 +255,11 @@ field_headings_visible=False, caption_visible=False ) %} + {% call row() %} + {{ text_field('Organisation')}} + {{ optional_text_field(organisation or 'Not Set') }} + {{ edit_field('Change', url_for('.link_service_to_organisation', service_id=current_service.id)) }} + {% endcall %} {% call row() %} {{ text_field('Organisation type')}} {{ optional_text_field( diff --git a/app/templates/views/service-settings/link-service-to-organisation.html b/app/templates/views/service-settings/link-service-to-organisation.html new file mode 100644 index 000000000..25c0c20df --- /dev/null +++ b/app/templates/views/service-settings/link-service-to-organisation.html @@ -0,0 +1,26 @@ +{% extends "withnav_template.html" %} +{% from "components/radios.html" import radios %} +{% from "components/page-footer.html" import page_footer %} + +{% block service_page_title %} + Link service to organisation +{% endblock %} + +{% block maincolumn_content %} + +
+
+

+ Link service to organisation +

+
+ {{ radios(form.organisations) }} + {{ page_footer( + 'Save', + back_link=url_for('.service_settings', service_id=current_service.id), + back_link_text='Back to settings' + ) }} +
+
+
+{% endblock %} 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 a126e454b..3762871a7 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 @@ -12,6 +12,7 @@ def get_service_settings_page( service_one, mock_get_inbound_number_for_service, mock_get_letter_email_branding, + mock_get_service_organisation, mock_get_free_sms_fragment_limit, no_reply_to_email_addresses, no_letter_contact_blocks, @@ -103,6 +104,7 @@ def test_normal_user_doesnt_see_any_toggle_buttons( service_one, no_reply_to_email_addresses, no_letter_contact_blocks, + mock_get_service_organisation, single_sms_sender, mock_get_letter_email_branding, mock_get_inbound_number_for_service, diff --git a/tests/app/main/views/test_organisations.py b/tests/app/main/views/test_organisations.py index 1249f7910..b59e5ed74 100644 --- a/tests/app/main/views/test_organisations.py +++ b/tests/app/main/views/test_organisations.py @@ -54,7 +54,7 @@ def test_view_organisation_shows_the_correct_organisation( assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') - assert page.select_one('.heading-large div').text == org['name'] + assert normalize_spaces(page.select_one('.heading-large').text) == org['name'] def test_edit_organisation_shows_the_correct_organisation( diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index ba31594c2..af52e4b82 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -83,6 +83,7 @@ def mock_get_service_settings_page_common( 'Send letters Off Change', 'Label Value Action', + 'Organisation Org 1 Change', 'Organisation type Central Change', 'Free text message allowance 250,000 Change', 'Email branding GOV.UK Change', @@ -97,6 +98,7 @@ def test_should_show_overview( fake_uuid, no_reply_to_email_addresses, no_letter_contact_blocks, + mock_get_service_organisation, single_sms_sender, user, expected_rows, @@ -169,6 +171,7 @@ def test_should_show_overview_for_service_with_more_things_set( single_reply_to_email_address, single_letter_contact_block, single_sms_sender, + mock_get_service_organisation, mock_get_email_branding, mock_get_service_settings_page_common, permissions, @@ -201,6 +204,7 @@ def test_letter_contact_block_shows_none_if_not_set( mocker, single_reply_to_email_address, no_letter_contact_blocks, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -221,6 +225,7 @@ def test_escapes_letter_contact_block( mocker, single_reply_to_email_address, single_sms_sender, + mock_get_service_organisation, injected_letter_contact_block, mock_get_service_settings_page_common, ): @@ -281,6 +286,7 @@ def test_show_restricted_service( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -315,6 +321,7 @@ def test_show_live_service( mock_get_live_service, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -449,6 +456,7 @@ def test_should_redirect_after_request_to_go_live( active_user_with_permissions, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common ): @@ -508,6 +516,7 @@ def test_route_permissions( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, route, mock_get_service_settings_page_common, @@ -565,6 +574,7 @@ def test_route_for_platform_admin( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, route, mock_get_service_settings_page_common, @@ -635,6 +645,7 @@ def test_and_more_hint_appears_on_settings_with_more_than_just_a_single_sender( service_one, multiple_reply_to_email_addresses, multiple_letter_contact_blocks, + mock_get_service_organisation, multiple_sms_senders, mock_get_service_settings_page_common, ): @@ -664,6 +675,7 @@ def test_api_ids_dont_show_on_option_pages_with_a_single_sender( client_request, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, sender_list_page, expected_output @@ -1218,6 +1230,7 @@ def test_shows_research_mode_indicator( mocker, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -1237,6 +1250,7 @@ def test_does_not_show_research_mode_indicator( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -1684,6 +1698,7 @@ def test_archive_service_prompts_user( mocker, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -1702,6 +1717,7 @@ def test_cant_archive_inactive_service( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common ): @@ -1735,6 +1751,7 @@ def test_suspend_service_prompts_user( mocker, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -1754,6 +1771,7 @@ def test_cant_suspend_inactive_service( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common, ): @@ -1771,6 +1789,7 @@ def test_resume_service_after_confirm( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, mocker, mock_get_inbound_number_for_service, ): @@ -1789,6 +1808,7 @@ def test_resume_service_prompts_user( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mocker, mock_get_service_settings_page_common, @@ -1810,6 +1830,7 @@ def test_cant_resume_active_service( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mock_get_service_settings_page_common ): @@ -1871,6 +1892,7 @@ def test_service_settings_when_inbound_number_is_not_set( service_one, single_reply_to_email_address, single_letter_contact_block, + mock_get_service_organisation, single_sms_sender, mocker, mock_get_letter_email_branding, @@ -1993,3 +2015,58 @@ def test_updates_sms_prefixing( service_id=SERVICE_ONE_ID, prefix_sms=expected_api_argument, ) + + +def test_select_organisation( + logged_in_platform_admin_client, + service_one, + mock_get_service_organisation, + mock_get_organisations +): + response = logged_in_platform_admin_client.get( + url_for('.link_service_to_organisation', service_id=service_one['id']), + ) + + assert response.status_code == 200 + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + + assert len(page.select('.multiple-choice')) == 3 + for i in range(0, 3): + assert normalize_spaces( + page.select('.multiple-choice label')[i].text + ) == 'Org {}'.format(i + 1) + + +def test_update_service_organisation( + logged_in_platform_admin_client, + service_one, + mock_get_service_organisation, + mock_get_organisations, + mock_update_service_organisation, +): + response = logged_in_platform_admin_client.post( + url_for('.link_service_to_organisation', service_id=service_one['id']), + data={'organisations': '7aa5d4e9-4385-4488-a489-07812ba13384'}, + ) + + assert response.status_code == 302 + mock_update_service_organisation.assert_called_once_with( + service_one['id'], + '7aa5d4e9-4385-4488-a489-07812ba13384' + ) + + +def test_update_service_organisation_does_not_update_if_same_value( + logged_in_platform_admin_client, + service_one, + mock_get_service_organisation, + mock_get_organisations, + mock_update_service_organisation, +): + response = logged_in_platform_admin_client.post( + url_for('.link_service_to_organisation', service_id=service_one['id']), + data={'organisations': '7aa5d4e9-4385-4488-a489-07812ba13383'}, + ) + + assert response.status_code == 302 + mock_update_service_organisation.called is False diff --git a/tests/conftest.py b/tests/conftest.py index eb6435a68..9b41cf22d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2631,3 +2631,62 @@ def mock_update_service_callback_api(mocker): return return mocker.patch('app.service_api_client.update_service_callback_api', side_effect=_update_service_callback_api) + + +@pytest.fixture(scope='function') +def mock_get_organisations(mocker): + def _get_organisations(): + return [ + { + 'name': 'Org 1', + 'id': '7aa5d4e9-4385-4488-a489-07812ba13383', + 'active': True + }, + { + 'name': 'Org 2', + 'id': '7aa5d4e9-4385-4488-a489-07812ba13384', + 'active': True + }, + { + 'name': 'Org 3', + 'id': '7aa5d4e9-4385-4488-a489-07812ba13385', + 'active': True + } + ] + + return mocker.patch('app.organisations_client.get_organisations', side_effect=_get_organisations) + + +@pytest.fixture(scope='function') +def mock_get_organisation(mocker): + def _get_organisation(organisation_id): + return { + 'name': 'Org 1', + 'id': organisation_id, + 'active': True + } + + return mocker.patch('app.organisations_client.get_organisation', side_effect=_get_organisation) + + +@pytest.fixture(scope='function') +def mock_get_service_organisation(mocker): + def _get_service_organisation(service_id): + return { + 'name': 'Org 1', + 'id': '7aa5d4e9-4385-4488-a489-07812ba13383', + 'active': True + } + + return mocker.patch('app.organisations_client.get_service_organisation', side_effect=_get_service_organisation) + + +@pytest.fixture(scope='function') +def mock_update_service_organisation(mocker): + def _update_service_organisation(service_id, organisation_id): + return + + return mocker.patch( + 'app.organisations_client.update_service_organisation', + side_effect=_update_service_organisation + )