diff --git a/app/assets/javascripts/emailPreviewPane.js b/app/assets/javascripts/previewPane.js similarity index 66% rename from app/assets/javascripts/emailPreviewPane.js rename to app/assets/javascripts/previewPane.js index 0509efa27..7a8172861 100644 --- a/app/assets/javascripts/emailPreviewPane.js +++ b/app/assets/javascripts/previewPane.js @@ -13,9 +13,8 @@ const $paneWrapper = $('
'); const $form = $('form'); - const $previewPane = $(''); + const previewType = $form.data('previewType'); + const $previewPane = $(``); function buildQueryString () { return $.map(arguments, (val, idx) => encodeURI(val[0]) + '=' + encodeURI(val[1])).join('&'); @@ -26,12 +25,12 @@ if ($target.attr('name') == 'branding_style') { branding_style = $target.val(); } - $previewPane.attr('src', '/_email?' + buildQueryString(['branding_style', branding_style])); + $previewPane.attr('src', `/_${previewType}?${buildQueryString(['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.attr('action', location.pathname.replace(new RegExp(`set-${previewType}-branding$`), `preview-${previewType}-branding`)); $form.find('button[type="submit"]').text('Save'); $('fieldset').on('change', 'input[name="branding_style"]', setPreviewPane); diff --git a/app/assets/stylesheets/components/email-preview-pane.scss b/app/assets/stylesheets/components/preview-pane.scss similarity index 94% rename from app/assets/stylesheets/components/email-preview-pane.scss rename to app/assets/stylesheets/components/preview-pane.scss index c615df2ad..12b8c28c5 100644 --- a/app/assets/stylesheets/components/email-preview-pane.scss +++ b/app/assets/stylesheets/components/preview-pane.scss @@ -1,4 +1,4 @@ -.email-branding-preview { +.branding-preview { width: 100%; box-sizing: border-box; border: solid 1px $border-colour; @@ -11,7 +11,7 @@ background-image: linear-gradient(45deg, $grey-3 25%, transparent 25%), linear-gradient(-45deg, $grey-3 25%, transparent 25%), linear-gradient(45deg, transparent 75%, $grey-3 75%), linear-gradient(-45deg, transparent 75%, $grey-3 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; - + img { display: block; } diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss index f8fc5787c..1db1fcff1 100644 --- a/app/assets/stylesheets/main.scss +++ b/app/assets/stylesheets/main.scss @@ -61,7 +61,7 @@ $path: '/static/images/'; @import 'components/conditional-radios'; @import 'components/vendor/breadcrumbs'; @import 'components/vendor/responsive-embed'; -@import 'components/email-preview-pane'; +@import 'components/preview-pane'; @import 'components/task-list'; @import 'views/dashboard'; diff --git a/app/main/views/index.py b/app/main/views/index.py index c68296f9a..1247c0aa2 100644 --- a/app/main/views/index.py +++ b/app/main/views/index.py @@ -10,9 +10,9 @@ 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 notifications_utils.template import HTMLEmailTemplate, LetterImageTemplate -from app import email_branding_client +from app import email_branding_client, letter_branding_client from app.main import main from app.main.forms import FieldWithNoneOption, SearchByNameForm from app.main.views.sub_navigation_dictionaries import features_nav @@ -174,6 +174,35 @@ def email_template(): return resp +@main.route('/_letter') +def letter_template(): + branding_style = request.args.get('branding_style') + + if branding_style == FieldWithNoneOption.NONE_OPTION_VALUE: + branding_style = None + + if branding_style: + filename = letter_branding_client.get_letter_branding(branding_style)['filename'] + else: + filename = 'no-branding' + + template = {'subject': '', 'content': ''} + image_url = url_for('main.letter_branding_preview_image', filename=filename) + + template_image = str(LetterImageTemplate( + template, + image_url=image_url, + page_count=1, + )) + + resp = make_response( + render_template('views/service-settings/letter-preview.html', template=template_image) + ) + + resp.headers['X-Frame-Options'] = 'SAMEORIGIN' + return resp + + @main.route('/documentation') def documentation(): return render_template('views/documentation.html') diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 69b477de2..063c8730a 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -810,6 +810,28 @@ def service_set_letter_branding(service_id): current_branding=current_service.letter_branding_id, ) + if form.validate_on_submit(): + return redirect(url_for( + '.service_preview_letter_branding', + service_id=service_id, + branding_style=form.branding_style.data, + )) + + return render_template( + 'views/service-settings/set-letter-branding.html', + form=form, + search_form=SearchByNameForm() + ) + + +@main.route("/services//service-settings/preview-letter-branding", methods=['GET', 'POST']) +@login_required +@user_is_platform_admin +def service_preview_letter_branding(service_id): + branding_style = request.args.get('branding_style') + + form = ServicePreviewBranding(branding_style=branding_style) + if form.validate_on_submit(): current_service.update( letter_branding=form.branding_style.data @@ -817,9 +839,10 @@ def service_set_letter_branding(service_id): return redirect(url_for('.service_settings', service_id=service_id)) return render_template( - 'views/service-settings/set-letter-branding.html', + 'views/service-settings/preview-letter-branding.html', form=form, - search_form=SearchByNameForm() + service_id=service_id, + action=url_for('main.service_preview_letter_branding', service_id=service_id), ) diff --git a/app/main/views/templates.py b/app/main/views/templates.py index 25d8dadba..3d72eb131 100644 --- a/app/main/views/templates.py +++ b/app/main/views/templates.py @@ -36,6 +36,7 @@ from app.utils import ( get_template, should_skip_template_page, user_has_permissions, + user_is_platform_admin, ) form_objects = { @@ -222,6 +223,34 @@ def view_letter_template_preview(service_id, template_id, filetype): return TemplatePreview.from_database_object(db_template, filetype, page=request.args.get('page')) +@main.route("/templates/letter-preview-image/") +@login_required +@user_is_platform_admin +def letter_branding_preview_image(filename): + template = { + 'subject': 'An example letter', + '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\nIt 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' + '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.' + ) + } + filename = None if filename == 'no-branding' else filename + + return TemplatePreview.from_example_template(template, filename) + + def _view_template_version(service_id, template_id, version, letters_as_pdf=False): return dict(template=get_template( service_api_client.get_service_template(service_id, template_id, version=version)['data'], diff --git a/app/navigation.py b/app/navigation.py index befb5e046..69a5814ca 100644 --- a/app/navigation.py +++ b/app/navigation.py @@ -173,6 +173,8 @@ class HeaderNavigation(Navigation): 'information_security', 'invite_org_user', 'invite_user', + 'letter_branding_preview_image', + 'letter_template', 'link_service_to_organisation', 'manage_org_users', 'manage_template_folder', @@ -228,6 +230,7 @@ class HeaderNavigation(Navigation): 'service_name_change', 'service_name_change_confirm', 'service_preview_email_branding', + 'service_preview_letter_branding', 'service_set_auth_type', 'service_set_channel', 'service_set_contact_link', @@ -235,6 +238,7 @@ class HeaderNavigation(Navigation): 'service_set_inbound_number', 'service_set_inbound_sms', 'service_set_international_sms', + 'service_set_letter_branding', 'service_set_letter_contact_block', 'service_set_letters', 'service_set_reply_to_email', @@ -248,7 +252,6 @@ class HeaderNavigation(Navigation): 'service_switch_research_mode', 'services_or_dashboard', 'set_free_sms_allowance', - 'service_set_letter_branding', 'set_organisation_type', 'set_sender', 'set_template_sender', @@ -348,6 +351,7 @@ class MainNavigation(Navigation): 'service_name_change', 'service_name_change_confirm', 'service_preview_email_branding', + 'service_preview_letter_branding', 'service_set_auth_type', 'service_set_channel', 'service_set_contact_link', @@ -436,7 +440,9 @@ class MainNavigation(Navigation): 'integration_testing', 'invite_org_user', 'letter_branding', + 'letter_branding_preview_image', 'live_services', + 'letter_template', 'manage_org_users', 'new_password', 'old_integration_testing', @@ -622,6 +628,8 @@ class CaseworkNavigation(Navigation): 'invite_org_user', 'invite_user', 'letter_branding', + 'letter_branding_preview_image', + 'letter_template', 'link_service_to_organisation', 'live_services', 'manage_org_users', @@ -682,6 +690,7 @@ class CaseworkNavigation(Navigation): 'service_name_change', 'service_name_change_confirm', 'service_preview_email_branding', + 'service_preview_letter_branding', 'service_set_auth_type', 'service_set_channel', 'service_set_contact_link', @@ -854,6 +863,8 @@ class OrgNavigation(Navigation): 'integration_testing', 'invite_user', 'letter_branding', + 'letter_branding_preview_image', + 'letter_template', 'link_service_to_organisation', 'live_services', 'manage_template_folder', @@ -914,6 +925,7 @@ class OrgNavigation(Navigation): 'service_name_change', 'service_name_change_confirm', 'service_preview_email_branding', + 'service_preview_letter_branding', 'service_set_auth_type', 'service_set_channel', 'service_set_contact_link', diff --git a/app/template_previews.py b/app/template_previews.py index b154bcc29..2dbcba122 100644 --- a/app/template_previews.py +++ b/app/template_previews.py @@ -24,6 +24,21 @@ class TemplatePreview: ) return (resp.content, resp.status_code, resp.headers.items()) + @classmethod + def from_example_template(cls, template, filename): + data = { + 'letter_contact_block': template.get('reply_to_text'), + 'template': template, + 'values': None, + 'filename': filename + } + resp = requests.post( + '{}/preview.png'.format(current_app.config['TEMPLATE_PREVIEW_API_HOST']), + json=data, + headers={'Authorization': 'Token {}'.format(current_app.config['TEMPLATE_PREVIEW_API_KEY'])} + ) + return (resp.content, resp.status_code, resp.headers.items()) + @classmethod def from_utils_template(cls, template, filetype, page=None): return cls.from_database_object( diff --git a/app/templates/views/service-settings/letter-preview.html b/app/templates/views/service-settings/letter-preview.html new file mode 100644 index 000000000..526b71065 --- /dev/null +++ b/app/templates/views/service-settings/letter-preview.html @@ -0,0 +1,31 @@ + + + + + + Letter preview + + + + + + + {{ template }} + + + diff --git a/app/templates/views/service-settings/preview-email-branding.html b/app/templates/views/service-settings/preview-email-branding.html index 20d5478d3..d966d0b03 100644 --- a/app/templates/views/service-settings/preview-email-branding.html +++ b/app/templates/views/service-settings/preview-email-branding.html @@ -9,7 +9,7 @@

Preview email branding

- + {% call form_wrapper(action=action) %}
{{ form.hidden_tag() }} diff --git a/app/templates/views/service-settings/preview-letter-branding.html b/app/templates/views/service-settings/preview-letter-branding.html new file mode 100644 index 000000000..4c84ce508 --- /dev/null +++ b/app/templates/views/service-settings/preview-letter-branding.html @@ -0,0 +1,24 @@ +{% extends "views/platform-admin/_base_template.html" %} +{% from "components/form.html" import form_wrapper %} +{% block per_page_title %} + Preview letter branding +{% endblock %} + +{% block platform_admin_content %} + +

Preview letter branding

+
+
+ + {% call form_wrapper(action=action) %} +
+ {{ form.hidden_tag() }} + +
+ {% endcall %} +
+
+{% endblock %} diff --git a/app/templates/views/service-settings/set-email-branding.html b/app/templates/views/service-settings/set-email-branding.html index 62ddd583d..886c02afa 100644 --- a/app/templates/views/service-settings/set-email-branding.html +++ b/app/templates/views/service-settings/set-email-branding.html @@ -11,7 +11,7 @@ {% block maincolumn_content %}

Set email branding

- {% call form_wrapper() %} + {% call form_wrapper(data_kwargs={'preview-type': 'email'}) %}
diff --git a/app/templates/views/service-settings/set-letter-branding.html b/app/templates/views/service-settings/set-letter-branding.html index d9aad44c1..c84047a3a 100644 --- a/app/templates/views/service-settings/set-letter-branding.html +++ b/app/templates/views/service-settings/set-letter-branding.html @@ -11,12 +11,20 @@ {% block maincolumn_content %}

Set letter branding

- {% call form_wrapper() %} - {{ live_search(target_selector='.multiple-choice', show=True, form=search_form, label='Search by name') }} - {{ radios(form.branding_style, hide_legend=True) }} + {% call form_wrapper(data_kwargs={'preview-type': 'letter'}) %} +
+
+
+
+
+
+ {{ live_search(target_selector='.multiple-choice', show=True, form=search_form, label='Search by name') }} + {{ radios(form.branding_style, hide_legend=True) }} +
+
{{ page_footer( - 'Save', + 'Preview', back_link=url_for('.service_settings', service_id=current_service.id), back_link_text='Back to settings' ) }} diff --git a/gulpfile.babel.js b/gulpfile.babel.js index cc9ea9b6f..9c4100cb4 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -74,7 +74,7 @@ gulp.task('javascripts', () => gulp paths.src + 'javascripts/errorTracking.js', paths.src + 'javascripts/preventDuplicateFormSubmissions.js', paths.src + 'javascripts/fullscreenTable.js', - paths.src + 'javascripts/emailPreviewPane.js', + paths.src + 'javascripts/previewPane.js', paths.src + 'javascripts/colourPreview.js', paths.src + 'javascripts/templateFolderForm.js', paths.src + 'javascripts/main.js' diff --git a/tests/app/main/views/test_index.py b/tests/app/main/views/test_index.py index 965c5b7e4..8c0884ffa 100644 --- a/tests/app/main/views/test_index.py +++ b/tests/app/main/views/test_index.py @@ -4,6 +4,7 @@ import pytest from bs4 import BeautifulSoup from flask import url_for +from app.main.forms import FieldWithNoneOption from tests.conftest import ( active_user_with_permissions, normalize_spaces, @@ -288,3 +289,41 @@ def test_email_branding_preview( **extra_args ) assert mock_get_email_branding.called is email_branding_retrieved + + +@pytest.mark.parametrize('branding_style, filename', [ + ('hm-government', 'hm-government'), + (None, 'no-branding'), + (FieldWithNoneOption.NONE_OPTION_VALUE, 'no-branding') +]) +def test_letter_template_preview_links_to_the_correct_image( + client_request, + mocker, + mock_get_letter_branding_by_id, + branding_style, + filename, +): + page = client_request.get( + 'main.letter_template', + _test_page_title=False, + branding_style=branding_style + ) + + image_link = page.find('img')['src'] + + assert image_link == url_for( + 'main.letter_branding_preview_image', + filename=filename, + page=1 + ) + + +def test_letter_template_preview_headers( + client, + mock_get_letter_branding_by_id, +): + response = client.get( + url_for('main.letter_template', branding_style='hm-government') + ) + + assert response.headers.get('X-Frame-Options') == 'SAMEORIGIN' diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 976ddacc8..552c9e75b 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -2175,7 +2175,44 @@ def test_service_set_letter_branding_prepopulates( (str(UUID(int=1)), str(UUID(int=1))), ('__NONE__', None), ]) -def test_service_set_letter_branding_saves( +def test_service_set_letter_branding_redirects_to_preview_page_when_form_submitted( + logged_in_platform_admin_client, + service_one, + mock_get_all_letter_branding, + selected_letter_branding, + expected_post_data +): + response = logged_in_platform_admin_client.post( + url_for('main.service_set_letter_branding', service_id=service_one['id']), + data={'branding_style': selected_letter_branding}, + ) + assert response.status_code == 302 + assert response.location == url_for( + 'main.service_preview_letter_branding', + service_id=service_one['id'], + branding_style=expected_post_data, + _external=True) + + +def test_service_preview_letter_branding_shows_preview_letter( + logged_in_platform_admin_client, + service_one, + mock_get_all_letter_branding, +): + response = logged_in_platform_admin_client.get( + url_for('main.service_preview_letter_branding', service_id=service_one['id'], branding_style='hm-government') + ) + page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') + + assert response.status_code == 200 + assert page.find('iframe')['src'] == url_for('main.letter_template', branding_style='hm-government') + + +@pytest.mark.parametrize('selected_letter_branding, expected_post_data', [ + (str(UUID(int=1)), str(UUID(int=1))), + ('__NONE__', None), +]) +def test_service_preview_letter_branding_saves( logged_in_platform_admin_client, service_one, mock_update_service, @@ -2184,7 +2221,7 @@ def test_service_set_letter_branding_saves( expected_post_data ): response = logged_in_platform_admin_client.post( - url_for('main.service_set_letter_branding', service_id=service_one['id']), + url_for('main.service_preview_letter_branding', service_id=service_one['id']), data={'branding_style': selected_letter_branding} ) assert response.status_code == 302 @@ -2276,7 +2313,7 @@ def test_should_preview_email_branding( )) assert response.status_code == 200 page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') - iframe = page.find('iframe', attrs={"class": "email-branding-preview"}) + iframe = page.find('iframe', attrs={"class": "branding-preview"}) iframeURLComponents = urlparse(iframe['src']) iframeQString = parse_qs(iframeURLComponents.query) diff --git a/tests/app/main/views/test_templates.py b/tests/app/main/views/test_templates.py index 175cabc77..725c15489 100644 --- a/tests/app/main/views/test_templates.py +++ b/tests/app/main/views/test_templates.py @@ -798,6 +798,28 @@ def test_dont_show_preview_letter_templates_for_bad_filetype( assert mock_get_service_template.called is False +@pytest.mark.parametrize('original_filename, new_filename', [ + ('geo', 'geo'), + ('no-branding', None) +]) +def test_letter_branding_preview_image( + mocker, + logged_in_platform_admin_client, + original_filename, + new_filename, +): + mocked_preview = mocker.patch( + 'app.main.views.templates.TemplatePreview.from_example_template', + return_value='foo' + ) + resp = logged_in_platform_admin_client.get( + url_for('.letter_branding_preview_image', filename=original_filename) + ) + + mocked_preview.assert_called_with(ANY, new_filename) + assert resp.get_data(as_text=True) == 'foo' + + def test_choosing_to_copy_redirects( client_request, service_one, diff --git a/tests/app/test_template_previews.py b/tests/app/test_template_previews.py index fdf78f345..dbfcbd159 100644 --- a/tests/app/test_template_previews.py +++ b/tests/app/test_template_previews.py @@ -102,3 +102,20 @@ def test_page_count_unpacks_from_json_response( assert partial_call({'template_type': 'letter'}) == 99 mock_template_preview.assert_called_once_with(*expected_template_preview_args) + + +def test_from_example_template_makes_request(mocker): + request_mock = mocker.patch('app.template_previews.requests.post') + template = {} + filename = 'geo' + + TemplatePreview.from_example_template(template, filename) + + request_mock.assert_called_once_with( + 'http://localhost:9999/preview.png', + headers={'Authorization': 'Token my-secret-key'}, + json={'values': None, + 'template': template, + 'filename': filename, + 'letter_contact_block': None} + )