Merge pull request #1037 from alphagov/pdf-preview

Render letter templates as PNGs and PDFs
This commit is contained in:
Chris Hill-Scott
2016-12-14 17:16:48 +01:00
committed by GitHub
21 changed files with 206 additions and 103 deletions

View File

@@ -25,7 +25,7 @@ Languages needed
- [Node](https://nodejs.org/) 5.0.0 or greater
- [npm](https://www.npmjs.com/) 3.0.0 or greater
```shell
brew install node
brew install node imagemagick ghostscript cairo pango
```
[NPM](npmjs.org) is Node's package management tool. `n` is a tool for managing

View File

@@ -1,6 +1,7 @@
$outline-width: 5px;
.letter {
font-family: Helvetica, Arial, sans-serif;
box-shadow:
1px 1px 0 0 $panel-colour,
@@ -8,6 +9,16 @@ $outline-width: 5px;
-1px 1px 0 0 $panel-colour,
-2px 2px 0 0 rgba($panel-colour, 0.5);
outline: $outline-width solid rgba($text-colour, 0.1);
padding: 20px;
padding: 0;
margin: $outline-width $outline-width $gutter;
a {
display: block;
}
img {
display: block;
max-width: 100%;
}
}

View File

@@ -3,8 +3,7 @@ from app.main import main
from app import convert_to_boolean
from flask_login import (login_required, current_user)
from notifications_utils.renderers import HTMLEmail
from notifications_utils.template import HTMLEmailTemplate
@main.route('/')
@@ -57,9 +56,7 @@ def terms():
@main.route('/_email')
def email_template():
return HTMLEmail(
govuk_banner=convert_to_boolean(request.args.get('govuk_banner', True))
)(
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 '
@@ -96,7 +93,8 @@ def email_template():
'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))
))
@main.route('/documentation')

View File

@@ -16,7 +16,6 @@ from flask import (
)
from flask_login import login_required
from werkzeug.datastructures import MultiDict
from notifications_utils.template import Template
from app import (
job_api_client,
@@ -31,7 +30,8 @@ from app.utils import (
generate_previous_dict,
user_has_permissions,
generate_notifications_csv,
get_help_argument
get_help_argument,
get_template,
)
from app.statistics_utils import add_rate_to_job
@@ -108,14 +108,13 @@ def view_job(service_id, job_id):
'views/jobs/job.html',
finished=(total_notifications == processed_notifications),
uploaded_file_name=job['original_file_name'],
template=Template(
template=get_template(
service_api_client.get_service_template(
service_id=service_id,
template_id=job['template'],
version=job['template_version']
)['data'],
prefix=current_service['name'],
sms_sender=current_service['sms_sender']
current_service,
),
status=request.args.get('status', ''),
updates_url=url_for(

View File

@@ -18,7 +18,6 @@ from flask import (
from flask_login import login_required, current_user
from notifications_utils.columns import Columns
from notifications_utils.template import Template
from notifications_utils.recipients import RecipientCSV, first_column_headings, validate_and_format_phone_number
from app.main import main
@@ -28,7 +27,7 @@ from app.main.uploader import (
s3download
)
from app import job_api_client, service_api_client, current_service, user_api_client
from app.utils import user_has_permissions, get_errors_for_csv, Spreadsheet, get_help_argument, get_renderer
from app.utils import user_has_permissions, get_errors_for_csv, Spreadsheet, get_help_argument, get_template
def get_page_headings(template_type):
@@ -88,10 +87,8 @@ def choose_template(service_id, template_type):
return render_template(
'views/templates/choose.html',
templates=[
Template(
template,
renderer=get_renderer(template_type, current_service, show_recipient=False)
) for template in service_api_client.get_service_templates(service_id)['data']
get_template(template, current_service)
for template in service_api_client.get_service_templates(service_id)['data']
if template['template_type'] == template_type
],
template_type=template_type,
@@ -103,10 +100,12 @@ def choose_template(service_id, template_type):
@login_required
@user_has_permissions('send_texts', 'send_emails', 'send_letters')
def send_messages(service_id, template_id):
template = Template(
service_api_client.get_service_template(service_id, template_id)['data']
template = get_template(
service_api_client.get_service_template(service_id, template_id)['data'],
current_service,
show_recipient=True
)
template.renderer = get_renderer(template.template_type, current_service, show_recipient=True)
form = CsvUploadForm()
if form.validate_on_submit():
@@ -144,7 +143,9 @@ def send_messages(service_id, template_id):
@login_required
@user_has_permissions('send_texts', 'send_emails', 'send_letters', 'manage_templates', any_=True)
def get_example_csv(service_id, template_id):
template = Template(service_api_client.get_service_template(service_id, template_id)['data'])
template = get_template(
service_api_client.get_service_template(service_id, template_id)['data'], current_service
)
return Spreadsheet.from_rows([
first_column_headings[template.template_type] + list(template.placeholders),
get_example_csv_rows(template)
@@ -161,14 +162,12 @@ def send_test(service_id, template_id):
file_name = current_app.config['TEST_MESSAGE_FILENAME']
template = Template(
template = get_template(
service_api_client.get_service_template(service_id, template_id)['data'],
prefix=current_service['name'],
sms_sender=current_service['sms_sender']
current_service,
show_recipient=True
)
template.renderer = get_renderer(template.template_type, current_service, show_recipient=True)
if len(template.placeholders) == 0 or request.method == 'POST':
upload_id = s3upload(
service_id,
@@ -208,10 +207,8 @@ def send_test(service_id, template_id):
def send_from_api(service_id, template_id):
return render_template(
'views/send-from-api.html',
template=Template(
service_api_client.get_service_template(service_id, template_id)['data'],
prefix=current_service['name'],
sms_sender=current_service['sms_sender']
template=get_template(
service_api_client.get_service_template(service_id, template_id)['data'], current_service
)
)
@@ -233,15 +230,15 @@ def check_messages(service_id, template_type, upload_id):
if not contents:
flash('There was a problem reading your upload file')
template = Template(
template = get_template(
service_api_client.get_service_template(
service_id,
session['upload_data'].get('template_id')
)['data']
)['data'],
current_service,
show_recipient=True
)
template.renderer = get_renderer(template_type, current_service, show_recipient=True)
recipients = RecipientCSV(
contents,
template_type=template.template_type,

View File

@@ -1,16 +1,19 @@
from datetime import datetime, timedelta
from io import BytesIO
from string import ascii_uppercase
from flask import request, render_template, redirect, url_for, flash, abort
from flask import request, render_template, redirect, url_for, flash, abort, send_file
from flask_login import login_required
from flask_weasyprint import HTML, render_pdf
from dateutil.parser import parse
from wand.image import Image
from notifications_utils.template import Template
from notifications_utils.template import LetterPreviewTemplate
from notifications_utils.recipients import first_column_headings
from notifications_python_client.errors import HTTPError
from app.main import main
from app.utils import user_has_permissions, get_renderer
from app.utils import user_has_permissions, get_template
from app.main.forms import SMSTemplateForm, EmailTemplateForm, LetterTemplateForm
from app.main.views.send import get_example_csv_rows
from app import service_api_client, current_service, template_statistics_client
@@ -39,18 +42,41 @@ page_headings = {
admin_override=True, any_=True
)
def view_template(service_id, template_id):
template = Template(
service_api_client.get_service_template(service_id, template_id)['data']
)
template.renderer = get_renderer(
template.template_type, current_service, show_recipient=False, expand_emails=True
)
return render_template(
'views/templates/template.html',
template=template
template=get_template(
service_api_client.get_service_template(service_id, template_id)['data'],
current_service,
expand_emails=True
)
)
@main.route("/services/<service_id>/templates/<template_id>.pdf")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def view_letter_template_as_pdf(service_id, template_id):
return render_pdf(HTML(string=str(
LetterPreviewTemplate(
service_api_client.get_service_template(service_id, template_id)['data'],
)
)))
@main.route("/services/<service_id>/templates/<template_id>.png")
@login_required
@user_has_permissions('view_activity', admin_override=True)
def view_letter_template_as_image(service_id, template_id):
output = BytesIO()
with Image(
blob=view_letter_template_as_pdf(service_id, template_id).get_data()
) as image:
with image.convert('png') as converted:
converted.save(file=output)
output.seek(0)
return send_file(output, mimetype='image/png')
@main.route("/services/<service_id>/templates/<template_id>/version/<int:version>")
@login_required
@user_has_permissions(
@@ -63,15 +89,13 @@ def view_template(service_id, template_id):
any_=True
)
def view_template_version(service_id, template_id, version):
template = Template(
service_api_client.get_service_template(service_id, template_id, version)['data']
)
template.renderer = get_renderer(
template.template_type, current_service, show_recipient=False, expand_emails=True
)
return render_template(
'views/templates/template_history.html',
template=template
template=get_template(
service_api_client.get_service_template(service_id, template_id, version)['data'],
current_service,
expand_emails=True
)
)
@@ -125,19 +149,23 @@ def edit_service_template(service_id, template_id):
if form.validate_on_submit():
subject = form.subject.data if hasattr(form, 'subject') else None
new_template = Template({
new_template = get_template({
'name': form.name.data,
'content': form.template_content.data,
'subject': subject,
'template_type': template['template_type'],
'id': template['id']
})
template_change = Template(template).compare_to(new_template)
}, current_service)
template_change = get_template(template, current_service).compare_to(new_template)
if template_change.has_different_placeholders and not request.form.get('confirm'):
return render_template(
'views/templates/breaking-change.html',
template_change=template_change,
new_template=new_template,
new_template={
'name': form.name.data,
'subject': subject,
'content': form.template_content.data,
},
column_headings=list(ascii_uppercase[:len(new_template.placeholders) + 1]),
example_rows=[
first_column_headings[new_template.template_type] + list(new_template.placeholders),
@@ -231,18 +259,12 @@ def delete_service_template(service_id, template_id):
any_=True
)
def view_template_versions(service_id, template_id):
versions = []
for template in service_api_client.get_service_template_versions(service_id, template_id)['data']:
template = Template(template)
template.renderer = get_renderer(
template.template_type, current_service, show_recipient=False, expand_emails=True
)
versions.append(template)
return render_template(
'views/templates/choose_history.html',
versions=versions
versions=[
get_template(template, current_service, expand_emails=True)
for template in service_api_client.get_service_template_versions(service_id, template_id)['data']
]
)

View File

@@ -127,7 +127,7 @@
{% endif %}
{{ template.rendered }}
{{ template|string }}
{% if errors %}
{% if request.args.from_test %}

View File

@@ -12,7 +12,7 @@
{{ uploaded_file_name }}
</h1>
{{ template.rendered }}
{{ template|string }}
{{ ajax_block(partials, updates_url, 'status', finished=finished) }}
{{ ajax_block(partials, updates_url, 'counts', finished=finished) }}

View File

@@ -11,7 +11,7 @@
API info
</h1>
{{ template.rendered }}
{{ template|string }}
<div class="bottom-gutter">
{{ api_key(template.id, name="Template ID", thing='template ID') }}

View File

@@ -19,7 +19,7 @@
<h1 class="heading-large">Send yourself a test</h1>
{% endif %}
{{ template.rendered }}
{{ template|string }}
<form method="post">
{% call(item, row_number) list_table(

View File

@@ -11,7 +11,7 @@
<h1 class="heading-large">Upload recipients</h1>
{{ template.rendered }}
{{ template|string }}
<div class="page-footer bottom-gutter">
{{file_upload(

View File

@@ -1,5 +1,5 @@
<div class="column-two-thirds">
{{ template.rendered }}
{{ template|string }}
</div>
<div class="column-one-third">
{% if template._template.archived %}

View File

@@ -3,7 +3,7 @@
</div>
<div class="column-two-thirds">
{{ template.rendered }}
{{ template|string }}
</div>
<div class="column-one-third">

View File

@@ -8,7 +8,11 @@ import unicodedata
from flask import (abort, current_app, session, request, redirect, url_for)
from flask_login import current_user
from notifications_utils.renderers import SMSPreview, EmailPreview, LetterPreview
from notifications_utils.template import (
SMSPreviewTemplate,
EmailPreviewTemplate,
LetterPDFLinkTemplate,
)
import pyexcel
import pyexcel.ext.io
@@ -220,18 +224,24 @@ def is_gov_user(email_address):
return bool(re.search(email_regex, email_address.lower()))
def get_renderer(template_type, service, show_recipient, expand_emails=False):
return {
'email': EmailPreview(
def get_template(template, service, show_recipient=False, expand_emails=False):
if 'email' == template['template_type']:
return EmailPreviewTemplate(
template,
from_name=service['name'],
from_address='{}@notifications.service.gov.uk'.format(service['email_from']),
expanded=expand_emails,
show_recipient=show_recipient
),
'sms': SMSPreview(
)
if 'sms' == template['template_type']:
return SMSPreviewTemplate(
template,
prefix=service['name'],
sender=service['sms_sender'],
show_recipient=show_recipient
),
'letter': LetterPreview(),
}[template_type]
)
if 'letter' == template['template_type']:
return LetterPDFLinkTemplate(
template,
service_id=service['id'],
)

View File

@@ -26,6 +26,16 @@ RUN \
python-dev \
libffi-dev \
libssl-dev \
libexif-dev \
libfreetype6-dev \
libjpeg-dev \
liblcms2-2 \
libtiff5-dev \
zlib1g-dev \
libpango1.0-dev \
libcairo2-dev \
libmagickwand-dev \
ghostscript \
&& echo "Install nodejs" \
&& cd /tmp \

View File

@@ -3,6 +3,9 @@ Flask==0.10.1
Flask-Script==2.0.5
Flask-WTF==0.11
Flask-Login==0.3.2
Flask-WeasyPrint==0.5
git+https://github.com/quis/WeasyPrint.git@older-html5lib#egg=WeasyPrint==0.33
html5lib==1.0b8
credstash==1.8.0
boto3==1.3.0
Pygments==2.0.2
@@ -15,7 +18,8 @@ pyexcel-xls==0.1.0
pyexcel-xlsx==0.1.0
pyexcel-ods3==0.1.1
pytz==2016.4
wand==0.4.4
git+https://github.com/alphagov/notifications-python-client.git@3.0.1#egg=notifications-python-client==3.0.1
git+https://github.com/alphagov/notifications-utils.git@11.0.2#egg=notifications-utils==11.0.2
git+https://github.com/alphagov/notifications-utils.git@12.1.1#egg=notifications-utils==12.1.1

View File

@@ -3,21 +3,19 @@ from flask import url_for
@pytest.mark.parametrize(
"query_args, params", [
({}, {'govuk_banner': True}),
({'govuk_banner': 'false'}, {'govuk_banner': False})
"query_args, result", [
({}, True),
({'govuk_banner': 'false'}, 'false')
]
)
def test_renders(app_, mocker, query_args, params):
def test_renders(app_, mocker, query_args, result):
with app_.test_request_context(), app_.test_client() as client:
mock_html_email = mocker.patch(
'app.main.views.index.HTMLEmail',
return_value=lambda x: 'rendered'
)
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_html_email.assert_called_once_with(**params)
mock_convert_to_boolean.assert_called_once_with(result)

View File

@@ -163,7 +163,7 @@ def test_should_show_scheduled_job(
assert response.status_code == 200
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
assert page.find('main').find_all('p')[2].text.strip() == 'Sending will start today at midnight'
assert page.find('main').find_all('p')[1].text.strip() == 'Sending will start today at midnight'
assert page.find('input', {'type': 'submit', 'value': 'Cancel sending'})

View File

@@ -175,7 +175,7 @@ def test_send_test_email_message(
api_user_active,
mock_login,
mock_get_service,
mock_get_service_email_template,
mock_get_service_email_template_without_placeholders,
mock_s3_upload,
mock_has_permissions,
mock_get_users_by_service,

View File

@@ -1,5 +1,6 @@
from functools import partial
from datetime import datetime
from unittest.mock import Mock
from unittest.mock import Mock, patch
import pytest
from bs4 import BeautifulSoup
@@ -39,6 +40,39 @@ def test_should_show_page_for_one_template(
service_id, template_id)
@pytest.mark.parametrize(
'view, expected_content_type',
[
('.view_letter_template_as_pdf', 'application/pdf'),
('.view_letter_template_as_image', 'image/png'),
]
)
@patch("app.main.views.templates.LetterPreviewTemplate")
def test_should_show_preview_letter_templates(
mock_letter_preview,
view,
expected_content_type,
client,
api_user_active,
mock_login,
mock_get_service,
mock_get_service_email_template,
mock_get_user,
mock_get_user_by_email,
mock_has_permissions,
fake_uuid
):
client.login(api_user_active)
service_id = fake_uuid
template_id = fake_uuid
response = client.get(url_for(view, service_id=service_id, template_id=template_id))
assert response.status_code == 200
assert response.content_type == expected_content_type
mock_get_service_email_template.assert_called_with(service_id, template_id)
assert mock_letter_preview.call_args[0][0]['content'] == "Your vehicle tax expires on ((date))"
def test_should_redirect_when_saving_a_template(app_,
api_user_active,
mock_login,
@@ -79,7 +113,7 @@ def test_should_show_interstitial_when_making_breaking_change(
app_,
api_user_active,
mock_login,
mock_get_service_template,
mock_get_service_email_template,
mock_update_service_template,
mock_get_user,
mock_get_service,
@@ -97,8 +131,9 @@ def test_should_show_interstitial_when_making_breaking_change(
data={
'id': template_id,
'name': "new name",
'template_content': "hello ((name))",
'template_type': 'sms',
'template_content': "hello",
'template_type': 'email',
'subject': 'reminder',
'service': service_id
}
)
@@ -109,8 +144,8 @@ def test_should_show_interstitial_when_making_breaking_change(
for key, value in {
'name': 'new name',
'subject': '',
'template_content': 'hello ((name))',
'subject': 'reminder',
'template_content': 'hello',
'confirm': 'true'
}.items():
assert page.find('input', {'name': key})['value'] == value
@@ -195,7 +230,7 @@ def test_should_redirect_when_saving_a_template_email(app_,
service_id = fake_uuid
template_id = fake_uuid
name = "new name"
content = "template content"
content = "template content ((thing)) ((date))"
subject = "subject"
data = {
'id': template_id,

View File

@@ -322,7 +322,26 @@ def mock_get_service_email_template(mocker):
template_id,
"Two week reminder",
"email",
"Your vehicle tax is about to expire", "Subject")
"Your vehicle tax expires on ((date))",
"Your ((thing)) is due soon"
)
return {'data': template}
return mocker.patch(
'app.service_api_client.get_service_template', side_effect=_create)
@pytest.fixture(scope='function')
def mock_get_service_email_template_without_placeholders(mocker):
def _create(service_id, template_id):
template = template_json(
service_id,
template_id,
"Two week reminder",
"email",
"Your vehicle tax expires soon",
"Your thing is due soon"
)
return {'data': template}
return mocker.patch(