diff --git a/app/__init__.py b/app/__init__.py index de4cc583c..37781f2ef 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -514,7 +514,7 @@ def useful_headers_after_request(response): "object-src 'self';" "font-src 'self' data:;" "img-src 'self' *.google-analytics.com *.notifications.service.gov.uk {} data:;" - "frame-src www.youtube.com;".format(get_cdn_domain()) + "frame-src 'self' www.youtube.com;".format(get_cdn_domain()) )) if 'Cache-Control' in response.headers: del response.headers['Cache-Control'] diff --git a/app/assets/javascripts/emailPreviewPane.js b/app/assets/javascripts/emailPreviewPane.js new file mode 100644 index 000000000..7e42e99ba --- /dev/null +++ b/app/assets/javascripts/emailPreviewPane.js @@ -0,0 +1,43 @@ +(function () { + + 'use strict'; + + const root = this, + $ = this.jQuery; + + let branding_type = $('.multiple-choice input[name="branding_type"]:checked'); + let branding_style = $('.multiple-choice input[name="branding_style"]:checked'); + + if (!branding_type.length || !branding_style.length) { return; } + + branding_type = branding_type.val(); + branding_style = branding_style.val(); + + const $paneWrapper = $('
'); + const $form = $('form'); + const $previewPane = $(''); + + function buildQueryString () { + return $.map(arguments, (val, idx) => encodeURI(val[0]) + '=' + encodeURI(val[1])).join('&'); + } + + function setPreviewPane (e) { + const $target = $(e.target); + if ($target.attr('name') == 'branding_type') { + branding_type = $target.val(); + } + if ($target.attr('name') == 'branding_style') { + branding_style = $target.val(); + } + $previewPane.attr('src', '/_email?' + buildQueryString(['branding_type', branding_type], ['branding_style', branding_style])); + } + + $paneWrapper.append($previewPane); + $form.find('.grid-row').eq(0).prepend($paneWrapper); + $form.attr('action', location.pathname.replace(/set-email-branding$/, 'preview-email-branding')); + $form.find('button[type="submit"]').text('Save'); + + $('fieldset').on('change', 'input[name="branding_type"], input[name="branding_style"]', setPreviewPane); +})(); diff --git a/app/assets/stylesheets/components/email-preview-pane.scss b/app/assets/stylesheets/components/email-preview-pane.scss new file mode 100644 index 000000000..807069631 --- /dev/null +++ b/app/assets/stylesheets/components/email-preview-pane.scss @@ -0,0 +1,7 @@ +.email-branding-preview { + width: 100%; + box-sizing: border-box; + border: solid 1px $border-colour; + min-height: 200px; + margin-bottom: $gutter +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index 43edbaed5..9f5955cae 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -61,6 +61,7 @@ $path: '/static/images/'; @import 'components/conditional-radios'; @import 'components/vendor/breadcrumbs'; @import 'components/vendor/responsive-embed'; +@import 'components/email-preview-pane'; @import 'views/dashboard'; @import 'views/users'; diff --git a/app/main/forms.py b/app/main/forms.py index 988afb8b8..c99a93319 100644 --- a/app/main/forms.py +++ b/app/main/forms.py @@ -717,6 +717,12 @@ class ServiceSetBranding(StripWhitespaceForm): ) +class ServicePreviewBranding(StripWhitespaceForm): + + branding_type = HiddenField('branding_type') + branding_style = HiddenField('branding_style') + + class ServiceSelectEmailBranding(StripWhitespaceForm): def __init__(self, email_brandings=[], *args, **kwargs): diff --git a/app/main/views/index.py b/app/main/views/index.py index 77ba91b94..f0ee6f746 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -1,15 +1,22 @@ -from flask import abort, redirect, render_template, request, url_for +from flask import ( + abort, + make_response, + redirect, + render_template, + request, + url_for, +) from flask_login import current_user, login_required from notifications_utils.international_billing_rates import ( INTERNATIONAL_BILLING_RATES, ) from notifications_utils.template import HTMLEmailTemplate -from app import convert_to_boolean +from app import email_branding_client from app.main import main from app.main.forms import SearchTemplatesForm from app.main.views.sub_navigation_dictionaries import features_nav -from app.utils import AgreementInfo +from app.utils import AgreementInfo, get_cdn_domain @main.route('/') @@ -73,45 +80,79 @@ def design_content(): @main.route('/_email') def email_template(): - return str(HTMLEmailTemplate({'subject': 'foo', 'content': ( - 'Lorem Ipsum is simply dummy text of the printing and typesetting ' - 'industry.\n\nLorem Ipsum has been the industry’s standard dummy ' - 'text ever since the 1500s, when an unknown printer took a galley ' - 'of type and scrambled it to make a type specimen book. ' - '\n\n' - '# History' - '\n\n' - 'It has ' - 'survived not only' - '\n\n' - '* five centuries' - '\n' - '* but also the leap into electronic typesetting' - '\n\n' - 'It was ' - 'popularised in the 1960s with the release of Letraset sheets ' - 'containing Lorem Ipsum passages, and more recently with desktop ' - 'publishing software like Aldus PageMaker including versions of ' - 'Lorem Ipsum.' - '\n\n' - '^ It is a long established fact that a reader will be distracted ' - 'by the readable content of a page when looking at its layout.' - '\n\n' - 'The point of using Lorem Ipsum is that it has a more-or-less ' - 'normal distribution of letters, as opposed to using ‘Content ' - 'here, content here’, making it look like readable English.' - '\n\n\n' - '1. One' - '\n' - '2. Two' - '\n' - '10. Three' - '\n\n' - 'This is an example of an email sent using GOV.UK Notify.' - '\n\n' - 'https://www.notifications.service.gov.uk' - )}, govuk_banner=convert_to_boolean(request.args.get('govuk_banner', True)) - )) + branding_type = request.args.get('branding_type', 'govuk') + branding_style = request.args.get('branding_style', 'None') + + if branding_type == 'govuk' or branding_style == 'None': + brand_name = None + brand_colour = None + brand_logo = None + govuk_banner = True + brand_banner = False + else: + email_branding = email_branding_client.get_email_branding(branding_style)['email_branding'] + brand_name = email_branding['name'] + brand_colour = email_branding['colour'] + brand_logo = 'https://{}/{}'.format(get_cdn_domain(), email_branding['logo']) + govuk_banner = branding_type in ['govuk', 'both'] + brand_banner = branding_type == 'org_banner' + + template = { + 'subject': 'foo', + 'content': ( + 'Lorem Ipsum is simply dummy text of the printing and typesetting ' + 'industry.\n\nLorem Ipsum has been the industry’s standard dummy ' + 'text ever since the 1500s, when an unknown printer took a galley ' + 'of type and scrambled it to make a type specimen book. ' + '\n\n' + '# History' + '\n\n' + 'It has ' + 'survived not only' + '\n\n' + '* five centuries' + '\n' + '* but also the leap into electronic typesetting' + '\n\n' + 'It was ' + 'popularised in the 1960s with the release of Letraset sheets ' + 'containing Lorem Ipsum passages, and more recently with desktop ' + 'publishing software like Aldus PageMaker including versions of ' + 'Lorem Ipsum.' + '\n\n' + '^ It is a long established fact that a reader will be distracted ' + 'by the readable content of a page when looking at its layout.' + '\n\n' + 'The point of using Lorem Ipsum is that it has a more-or-less ' + 'normal distribution of letters, as opposed to using ‘Content ' + 'here, content here’, making it look like readable English.' + '\n\n\n' + '1. One' + '\n' + '2. Two' + '\n' + '10. Three' + '\n\n' + 'This is an example of an email sent using GOV.UK Notify.' + '\n\n' + 'https://www.notifications.service.gov.uk' + ) + } + + if not bool(request.args): + resp = make_response(str(HTMLEmailTemplate(template))) + else: + resp = make_response(str(HTMLEmailTemplate( + template, + govuk_banner=govuk_banner, + brand_name=brand_name, + brand_colour=brand_colour, + brand_logo=brand_logo, + brand_banner=brand_banner, + ))) + + resp.headers['X-Frame-Options'] = 'SAMEORIGIN' + return resp @main.route('/documentation') diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index fc1351cef..11b79cf49 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -41,6 +41,7 @@ from app.main.forms import ( ServiceEditInboundNumberForm, ServiceInboundNumberForm, ServiceLetterContactBlockForm, + ServicePreviewBranding, ServiceReplyToEmailForm, ServiceSetBranding, ServiceSmsSenderForm, @@ -895,12 +896,36 @@ def set_free_sms_allowance(service_id): @user_is_platform_admin def service_set_email_branding(service_id): email_branding = email_branding_client.get_all_email_branding() + branding_type = current_service.get('branding') - form = ServiceSetBranding(branding_type=current_service.get('branding')) + form = ServiceSetBranding(branding_type=branding_type) # dynamically create org choices, including the null option form.branding_style.choices = [('None', 'None')] + get_branding_as_value_and_label(email_branding) + if form.validate_on_submit(): + branding_style = None if form.branding_style.data == 'None' else form.branding_style.data + return redirect(url_for('.service_preview_email_branding', service_id=service_id, + branding_type=form.branding_type.data, branding_style=branding_style)) + + form.branding_style.data = current_service['email_branding'] or 'None' + + return render_template( + 'views/service-settings/set-email-branding.html', + form=form, + branding_dict=get_branding_as_dict(email_branding) + ) + + +@main.route("/services//service-settings/preview-email-branding", methods=['GET', 'POST']) +@login_required +@user_is_platform_admin +def service_preview_email_branding(service_id): + branding_type = request.args.get('branding_type', None) + branding_style = request.args.get('branding_style', None) + + form = ServicePreviewBranding(branding_type=branding_type, branding_style=branding_style) + if form.validate_on_submit(): branding_style = None if form.branding_style.data == 'None' else form.branding_style.data service_api_client.update_service( @@ -910,12 +935,11 @@ def service_set_email_branding(service_id): ) return redirect(url_for('.service_settings', service_id=service_id)) - form.branding_style.data = current_service.email_branding or 'None' - return render_template( - 'views/service-settings/set-email-branding.html', + 'views/service-settings/preview-email-branding.html', form=form, - branding_dict=get_branding_as_dict(email_branding) + service_id=service_id, + action=url_for('main.service_preview_email_branding', service_id=service_id), ) diff --git a/app/navigation.py b/app/navigation.py index ebc917a93..b4467cdcf 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -218,6 +218,7 @@ class HeaderNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', @@ -339,6 +340,7 @@ class MainNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', @@ -661,6 +663,7 @@ class CaseworkNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', @@ -887,6 +890,7 @@ class OrgNavigation(Navigation): 'service_letter_contact_details', 'service_name_change', 'service_name_change_confirm', + 'service_preview_email_branding', 'service_set_auth_type', 'service_set_basic_view', 'service_set_contact_link', diff --git a/app/templates/views/service-settings/preview-email-branding.html b/app/templates/views/service-settings/preview-email-branding.html new file mode 100644 index 000000000..51ecdb7e2 --- /dev/null +++ b/app/templates/views/service-settings/preview-email-branding.html @@ -0,0 +1,24 @@ +{% extends "views/platform-admin/_base_template.html" %} + +{% block service_page_title %} + Preview email branding +{% endblock %} + +{% block platform_admin_content %} + +

Preview email branding

+
+
+ +
+
+ {{ form.hidden_tag() }} + +
+
+
+
+{% endblock %} diff --git a/app/templates/views/service-settings/set-email-branding.html b/app/templates/views/service-settings/set-email-branding.html index 6aef3a71e..194d138a7 100644 --- a/app/templates/views/service-settings/set-email-branding.html +++ b/app/templates/views/service-settings/set-email-branding.html @@ -9,18 +9,24 @@ {% block maincolumn_content %}

Set email branding

-
-
-
+ +
+
{{ radios(form.branding_type) }} +
+
{{ branding_radios(form.branding_style, branding_dict=branding_dict) }} +
+
+
+
{{ page_footer( - 'Save', + 'Preview', back_link=url_for('.service_settings', service_id=current_service.id), back_link_text='Back to settings' ) }} - +
-
+ {% endblock %} diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 60b66f28a..a27093059 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -74,6 +74,7 @@ gulp.task('javascripts', () => gulp paths.src + 'javascripts/preventDuplicateFormSubmissions.js', paths.src + 'javascripts/fullscreenTable.js', paths.src + 'javascripts/conditionalRadios.js', + paths.src + 'javascripts/emailPreviewPane.js', paths.src + 'javascripts/main.js' ]) .pipe(plugins.prettyerror()) diff --git a/tests/app/main/views/test_email_preview.py b/tests/app/main/views/test_email_preview.py index 5b7923ce6..3d8fc34f7 100644 --- a/tests/app/main/views/test_email_preview.py +++ b/tests/app/main/views/test_email_preview.py @@ -1,4 +1,7 @@ +import re + import pytest +from bs4 import BeautifulSoup from flask import url_for @@ -10,11 +13,73 @@ from flask import url_for ) def test_renders(client, mocker, query_args, result): - mock_convert_to_boolean = mocker.patch('app.main.views.index.convert_to_boolean') mocker.patch('app.main.views.index.HTMLEmailTemplate.__str__', return_value='rendered') response = client.get(url_for('main.email_template', **query_args)) assert response.status_code == 200 assert response.get_data(as_text=True) == 'rendered' - mock_convert_to_boolean.assert_called_once_with(result) + + +def test_displays_govuk_branding_by_default(client): + + response = client.get(url_for('main.email_template')) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + + assert page.find("a", attrs={"href": "https://www.gov.uk"}) + + +def test_displays_govuk_branding(client): + + response = client.get(url_for('main.email_template', branding_type="govuk", branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + + assert page.find("a", attrs={"href": "https://www.gov.uk"}) + + +def test_displays_both_branding(client, mock_get_email_branding): + + response = client.get(url_for('main.email_template', branding_type="both", branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + mock_get_email_branding.assert_called_once_with('1') + + assert page.find("a", attrs={"href": "https://www.gov.uk"}) + assert page.find("img", attrs={"src": re.compile("example.png$")}) + + +def test_displays_org_branding(client, mock_get_email_branding): + + response = client.get(url_for('main.email_template', branding_type="org", branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + mock_get_email_branding.assert_called_once_with('1') + + assert not page.find("a", attrs={"href": "https://www.gov.uk"}) + assert page.find("img", attrs={"src": re.compile("example.png")}) + assert not page.select("body > table > tr > td[bgcolor='#f00']") + + +def test_displays_org_branding_with_banner(client, mock_get_email_branding): + + response = client.get(url_for('main.email_template', branding_type="org_banner", + branding_style="1")) + + page = BeautifulSoup(response.data.decode("utf-8"), "html.parser") + + assert response.status_code == 200 + mock_get_email_branding.assert_called_once_with('1') + + assert not page.find("a", attrs={"href": "https://www.gov.uk"}) + assert page.find("img", attrs={"src": re.compile("example.png")}) + assert page.select("body > table > tr > td[bgcolor='#f00']") diff --git a/tests/app/main/views/test_headers.py b/tests/app/main/views/test_headers.py index 94558d431..1ef2a794c 100644 --- a/tests/app/main/views/test_headers.py +++ b/tests/app/main/views/test_headers.py @@ -14,7 +14,7 @@ def test_owasp_useful_headers_set(client, mocker): "object-src 'self';" "font-src 'self' data:;" "img-src 'self' *.google-analytics.com *.notifications.service.gov.uk static-logos.test.com data:;" - "frame-src www.youtube.com;" + "frame-src 'self' www.youtube.com;" ) @@ -31,5 +31,5 @@ def test_headers_non_ascii_characters_are_replaced(client, mocker): "object-src 'self';" "font-src 'self' data:;" "img-src 'self' *.google-analytics.com *.notifications.service.gov.uk static-logos??.test.com data:;" - "frame-src www.youtube.com;" + "frame-src 'self' www.youtube.com;" ) diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 812891bb5..45cacf614 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -1769,7 +1769,7 @@ def test_should_show_organisations( app.service_api_client.get_service.assert_called_once_with(service_one['id']) -def test_should_set_branding_and_organisations( +def test_should_send_branding_and_organisations_to_preview( logged_in_platform_admin_client, service_one, mock_get_all_email_branding, @@ -1781,17 +1781,58 @@ def test_should_set_branding_and_organisations( ), data={ 'branding_type': 'org', - 'organisation': '1' + 'branding_style': '1' } ) assert response.status_code == 302 - assert response.location == url_for('main.service_settings', service_id=service_one['id'], _external=True) + assert response.location == url_for('main.service_preview_email_branding', + service_id=service_one['id'], branding_type='org', + branding_style='1', _external=True) mock_get_all_email_branding.assert_called_once_with() + + +def test_should_preview_email_branding( + logged_in_platform_admin_client, + service_one, +): + response = logged_in_platform_admin_client.get(url_for( + 'main.service_preview_email_branding', service_id=service_one['id'], + branding_type='org', branding_style='1' + )) + assert response.status_code == 200 + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + iframe = page.find('iframe', attrs={"class": "email-branding-preview"}) + + assert page.find('input', attrs={"id": "branding_type"})['value'] == 'org' + assert page.find('input', attrs={"id": "branding_style"})['value'] == '1' + assert iframe and iframe['src'] == '/_email?branding_type=org&branding_style=1' + + app.service_api_client.get_service.assert_called_once_with(service_one['id']) + + +def test_should_set_branding_and_organisations( + logged_in_platform_admin_client, + service_one, + mock_update_service, +): + response = logged_in_platform_admin_client.post( + url_for( + 'main.service_preview_email_branding', service_id=service_one['id'] + ), + data={ + 'branding_type': 'org', + 'branding_style': '1' + } + ) + assert response.status_code == 302 + assert response.location == url_for('main.service_settings', + service_id=service_one['id'], _external=True) + mock_update_service.assert_called_once_with( service_one['id'], branding='org', - email_branding=None + email_branding='1' )