Merge pull request #2209 from alphagov/add-email-branding-preview

Preview the email branding before setting it
This commit is contained in:
Tom Byers
2018-08-08 15:25:25 +01:00
committed by GitHub
14 changed files with 325 additions and 62 deletions

View File

@@ -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']

View File

@@ -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 = $('<div class="column-full"></div>');
const $form = $('form');
const $previewPane = $('<iframe src="/_email?' +
buildQueryString(['branding_type', branding_type], ['branding_style', branding_style]) +
'" class="email-branding-preview"></iframe>');
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);
})();

View File

@@ -0,0 +1,7 @@
.email-branding-preview {
width: 100%;
box-sizing: border-box;
border: solid 1px $border-colour;
min-height: 200px;
margin-bottom: $gutter
}

View File

@@ -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';

View File

@@ -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):

View File

@@ -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 industrys 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 industrys 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')

View File

@@ -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_id>/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),
)

View File

@@ -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',

View File

@@ -0,0 +1,24 @@
{% extends "views/platform-admin/_base_template.html" %}
{% block service_page_title %}
Preview email branding
{% endblock %}
{% block platform_admin_content %}
<h1 class="heading-large">Preview email branding</h1>
<div class="grid-row">
<div class="column-full">
<iframe src="{{ url_for('main.email_template', branding_type=form.branding_type.data, branding_style=form.branding_style.data) }}" class="email-branding-preview"></iframe>
<form method="post" action="{{ action }}">
<div class="form-group">
{{ form.hidden_tag() }}
<div class="page-footer">
<button type="submit" class="button">Save</button>
<a class="page-footer-back-link" href="{{ url_for('main.service_settings', service_id=service_id) }}">Back to service settings</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -9,18 +9,24 @@
{% block maincolumn_content %}
<h1 class="heading-large">Set email branding</h1>
<div class="grid-row">
<div class="column-three-quarters">
<form method="post">
<form method="post">
<div class="grid-row">
<div class="column-one-half">
{{ radios(form.branding_type) }}
</div>
<div class="column-one-half">
{{ branding_radios(form.branding_style, branding_dict=branding_dict) }}
</div>
</div>
<div class="grid-row">
<div class="column-three-quarters">
{{ page_footer(
'Save',
'Preview',
back_link=url_for('.service_settings', service_id=current_service.id),
back_link_text='Back to settings'
) }}
</form>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@@ -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())

View File

@@ -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']")

View File

@@ -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;"
)

View File

@@ -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'
)