diff --git a/app/__init__.py b/app/__init__.py index 2f39d2bee..d0e360b0d 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -45,6 +45,7 @@ from app.notify_client.template_statistics_api_client import TemplateStatisticsA from app.notify_client.user_api_client import UserApiClient from app.notify_client.events_api_client import EventsApiClient from app.notify_client.provider_client import ProviderClient +from app.notify_client.organisations_client import OrganisationsClient login_manager = LoginManager() csrf = CsrfProtect() @@ -60,6 +61,7 @@ statistics_api_client = StatisticsApiClient() template_statistics_client = TemplateStatisticsApiClient() events_api_client = EventsApiClient() provider_client = ProviderClient() +organisations_client = OrganisationsClient() asset_fingerprinter = AssetFingerprinter() # The current service attached to the request stack. @@ -88,6 +90,7 @@ def create_app(): template_statistics_client.init_app(application) events_api_client.init_app(application) provider_client.init_app(application) + organisations_client.init_app(application) login_manager.init_app(application) login_manager.login_view = 'main.sign_in' @@ -285,6 +288,8 @@ def load_user(user_id): def load_service_before_request(): + if '/static/' in request.url: + return service_id = request.view_args.get('service_id', session.get('service_id')) if request.view_args \ else session.get('service_id') from flask.globals import _request_ctx_stack diff --git a/app/main/forms.py b/app/main/forms.py index de1e1c7d8..c1f50a089 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -338,3 +338,29 @@ class ServiceSmsSender(Form): import re if field.data and not re.match('^[a-zA-Z0-9\s]+$', field.data): raise ValidationError('Sms text message sender can only contain alpha-numeric characters') + + +class ServiceBrandingOrg(Form): + + def __init__(self, organisations=[], *args, **kwargs): + self.organisation.choices = organisations + super(ServiceBrandingOrg, self).__init__(*args, **kwargs) + + branding_type = RadioField( + 'Branding', + choices=[ + ('govuk', 'GOV.UK only'), + ('both', 'GOV.UK and organisation'), + ('org', 'Organisation only') + ], + validators=[ + DataRequired() + ] + ) + + organisation = RadioField( + 'Organisation', + validators=[ + DataRequired() + ] + ) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 4e26adb1b..a49af5924 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -24,10 +24,10 @@ from app.main.forms import ( ServiceNameForm, RequestToGoLiveForm, ServiceReplyToEmailFrom, - ServiceSmsSender + ServiceSmsSender, + ServiceBrandingOrg ) -from app import user_api_client -from app import current_service +from app import user_api_client, current_service, organisations_client @main.route("/services//service-settings") @@ -266,3 +266,47 @@ def service_set_sms_sender(service_id): return render_template( 'views/service-settings/set-sms-sender.html', form=form) + + +@main.route("/services//service-settings/set-branding-and-org", methods=['GET', 'POST']) +@login_required +@user_has_permissions(admin_override=True) +def service_set_branding_and_org(service_id): + organisations = organisations_client.get_organisations() + + form = ServiceBrandingOrg(branding_type=current_service.get('branding')) + # dynamically create org choices, including the null option + form.organisation.choices = [('None', 'None')] + get_branding_as_value_and_label(organisations) + + if form.validate_on_submit(): + organisation = None if form.organisation.data == 'None' else form.organisation.data + service_api_client.update_service( + service_id, + branding=form.branding_type.data, + organisation=organisation + ) + return redirect(url_for('.service_settings', service_id=service_id)) + + form.organisation.data = current_service['organisation'] or 'None' + + return render_template( + 'views/service-settings/set-branding-and-org.html', + form=form, + branding_dict=get_branding_as_dict(organisations) + ) + + +def get_branding_as_value_and_label(organisations): + return [ + (organisation['id'], organisation['name']) + for organisation in organisations + ] + + +def get_branding_as_dict(organisations): + return { + organisation['id']: { + 'logo': '/static/images/email-template/crests/{}'.format(organisation['logo']), + 'colour': organisation['colour'] + } for organisation in organisations + } diff --git a/app/notify_client/organisations_client.py b/app/notify_client/organisations_client.py new file mode 100644 index 000000000..3d39decf7 --- /dev/null +++ b/app/notify_client/organisations_client.py @@ -0,0 +1,19 @@ +from notifications_python_client.base import BaseAPIClient + + +class OrganisationsClient(BaseAPIClient): + + def __init__(self, base_url=None, client_id=None, secret=None): + super(self.__class__, self).__init__( + base_url=base_url or 'base_url', + client_id=client_id or 'client_id', + secret=secret or 'secret' + ) + + def init_app(self, app): + self.base_url = app.config['API_HOST_NAME'] + self.client_id = app.config['ADMIN_CLIENT_USER_NAME'] + self.secret = app.config['ADMIN_CLIENT_SECRET'] + + def get_organisations(self): + return self.get(url='/organisation')['organisations'] diff --git a/app/notify_client/service_api_client.py b/app/notify_client/service_api_client.py index 718ef3946..0e67f28ca 100644 --- a/app/notify_client/service_api_client.py +++ b/app/notify_client/service_api_client.py @@ -90,7 +90,9 @@ class ServiceAPIClient(NotificationsAPIClient): 'email_from', 'reply_to_email_address', 'sms_sender', - 'created_by' + 'created_by', + 'branding', + 'organisation' } if disallowed_attributes: raise TypeError('Not allowed to update service attributes: {}'.format( diff --git a/app/templates/components/radios.html b/app/templates/components/radios.html index 88beb1220..2fba0f566 100644 --- a/app/templates/components/radios.html +++ b/app/templates/components/radios.html @@ -21,3 +21,44 @@ {% endmacro %} + + +{% macro branding_radios( + field, + hint=None, + branding_dict={} +) %} +
+
+ + {{ field.label }} + {% if field.errors %} + + {{ field.errors[0] }} + + {% endif %} + + {% for value, option, checked in field.iter_choices() %} + + {% endfor %} +
+
+{% endmacro %} diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index ca7eb6ce5..ccb2804d7 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -18,6 +18,10 @@ 'title': 'Set email reply to address', 'link': url_for('.service_set_reply_to_email', service_id=current_service.id) }, + { + 'title': 'Set email branding', + 'link': url_for('.service_set_branding_and_org', service_id=current_service.id) + } if current_user.has_permissions([], admin_override=True) else {}, { 'title': 'Set text message sender name', 'link': url_for('.service_set_sms_sender', service_id=current_service.id) @@ -51,7 +55,7 @@ 'title': 'Take service out of research mode', 'link': url_for('.service_switch_research_mode', service_id=current_service.id) } if current_service.research_mode and current_user.has_permissions([], admin_override=True) else { - }, + } ]) }} {% endblock %} diff --git a/app/templates/views/service-settings/set-branding-and-org.html b/app/templates/views/service-settings/set-branding-and-org.html new file mode 100644 index 000000000..b49b17a52 --- /dev/null +++ b/app/templates/views/service-settings/set-branding-and-org.html @@ -0,0 +1,25 @@ +{% extends "withnav_template.html" %} +{% from "components/radios.html" import radios, branding_radios %} +{% from "components/page-footer.html" import page_footer %} + +{% block page_title %} + Set branding and organisation – GOV.UK Notify +{% endblock %} + +{% block maincolumn_content %} + +

Set email branding

+
+
+
+ {{ radios(form.branding_type) }} + {{ branding_radios(form.organisation, branding_dict=branding_dict) }} + {{ page_footer( + 'Save', + back_link=url_for('.service_settings', service_id=current_service.id) + ) }} +
+
+
+ +{% endblock %} diff --git a/tests/__init__.py b/tests/__init__.py index 855abfe6a..6e1fef4e6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -49,7 +49,9 @@ def service_json( email_from=None, reply_to_email_address=None, sms_sender=None, - research_mode=False + research_mode=False, + organisation='organisation-id', + branding='govuk' ): return { 'id': id_, @@ -61,7 +63,9 @@ def service_json( 'email_from': email_from, 'reply_to_email_address': reply_to_email_address, 'sms_sender': sms_sender, - 'research_mode': research_mode + 'research_mode': research_mode, + 'organisation': organisation, + 'branding': branding } diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index bf769297b..8b214b647 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -759,3 +759,74 @@ def test_set_text_message_sender_flash_messages( element = page.find('div', {"class": "banner-default-with-tick"}) assert element.text.strip() == expected_flash_message + + +def test_should_show_branding( + mocker, app_, platform_admin_user, service_one, mock_get_organisations +): + with app_.test_request_context(), app_.test_client() as client: + client.login(platform_admin_user, mocker, service_one) + response = client.get(url_for( + 'main.service_set_branding_and_org', service_id=service_one['id'] + )) + assert response.status_code == 200 + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + + assert page.find('input', attrs={"id": "branding_type-0"})['value'] == 'govuk' + assert page.find('input', attrs={"id": "branding_type-1"})['value'] == 'both' + assert page.find('input', attrs={"id": "branding_type-2"})['value'] == 'org' + + assert 'checked' in page.find('input', attrs={"id": "branding_type-0"}).attrs + assert 'checked' not in page.find('input', attrs={"id": "branding_type-1"}).attrs + assert 'checked' not in page.find('input', attrs={"id": "branding_type-2"}).attrs + + app.organisations_client.get_organisations.assert_called_once_with() + app.service_api_client.get_service.assert_called_once_with(service_one['id']) + + +def test_should_show_organisations( + mocker, app_, platform_admin_user, service_one, mock_get_organisations +): + with app_.test_request_context(), app_.test_client() as client: + client.login(platform_admin_user, mocker, service_one) + response = client.get(url_for( + 'main.service_set_branding_and_org', service_id=service_one['id'] + )) + assert response.status_code == 200 + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + + assert page.find('input', attrs={"id": "branding_type-0"})['value'] == 'govuk' + assert page.find('input', attrs={"id": "branding_type-1"})['value'] == 'both' + assert page.find('input', attrs={"id": "branding_type-2"})['value'] == 'org' + + assert 'checked' in page.find('input', attrs={"id": "branding_type-0"}).attrs + assert 'checked' not in page.find('input', attrs={"id": "branding_type-1"}).attrs + assert 'checked' not in page.find('input', attrs={"id": "branding_type-2"}).attrs + + app.organisations_client.get_organisations.assert_called_once_with() + app.service_api_client.get_service.assert_called_once_with(service_one['id']) + + +def test_should_set_branding_and_organisations( + mocker, app_, platform_admin_user, service_one, mock_get_organisations, mock_update_service +): + with app_.test_request_context(), app_.test_client() as client: + client.login(platform_admin_user, mocker, service_one) + response = client.post( + url_for( + 'main.service_set_branding_and_org', service_id=service_one['id'] + ), + data={ + 'branding_type': 'org', + 'organisation': 'organisation-id' + } + ) + assert response.status_code == 302 + assert response.location == url_for('main.service_settings', service_id=service_one['id'], _external=True) + + app.organisations_client.get_organisations.assert_called_once_with() + app.service_api_client.update_service.assert_called_once_with( + service_one['id'], + branding='org', + organisation='organisation-id' + ) diff --git a/tests/conftest.py b/tests/conftest.py index 44357684b..25bde5c81 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1133,3 +1133,20 @@ def mock_events(mocker): @pytest.fixture(scope='function') def mock_send_already_registered_email(mocker): return mocker.patch('app.user_api_client.send_already_registered_email') + + +@pytest.fixture(scope='function') +def mock_get_organisations(mocker): + def _get_organisations(): + return [ + { + 'logo': 'example.png', + 'name': 'Organisation name', + 'id': 'organisation-id', + 'colour': '#f00' + } + ] + + return mocker.patch( + 'app.organisations_client.get_organisations', side_effect=_get_organisations + )