Skip to content

Commit

Permalink
Generate PDF reports from HTML using grover gem
Browse files Browse the repository at this point in the history
Before, the [`wicked_pdf` gem][wicked] was used to generate PDF files for the
means and merits reports from HTML templates. This gem relies on
[`wkhtmltopdf`][wk] to generate PDFs from HTML.

As part of the [work to upgrade to Ruby version to 3.1.3][ruby], we discovered that
`wkhtmltopdf` was no longer available as part the Alpine image.

As outlined in the repo, the project is no longer actively maintained —so we
took a decision to migrate away from that tool and consider alternatives.

The [`grover` gem][grover] makes use of [`puppeteer`][puppeteer] to render HTML
in a headless browser and save a PDF file. Other approaches were identified (e.g
writing PDFs [using `prawn`][prawn]), but ultimately `grover` was chosen as a
drop-in replacement.

This replaces `wicked_pdf` with `grover` for means and merits report creation
(which are subsequently sent to CCMS), and updates the respective controllers so
that PDF files can be rendered in the browser for testing purposes.

Ensuring Puppeteer and Chromium are available in each scenario (locally, unit
tests in CI, and in alpine environments) requires a slightly different
approach in each instance. This is not ideal, but it is necessary.

Puppeteer is not run in a sandbox environment (running as a non-root user in
Alpine is convoluted), but since the only PDFs we generate are from HTML we
write and control, this should not pose a security risk.

[wicked]: https://github.com/mileszs/wicked_pdf
[wk]: https://github.com/wkhtmltopdf/wkhtmltopdf
[ruby]: #4663 (comment)
[grover]: https://github.com/Studiosity/grover
[puppeteer]: https://github.com/puppeteer/puppeteer
[prawn]: https://github.com/prawnpdf/prawn
  • Loading branch information
cpjmcquillan committed Jan 6, 2023
1 parent 92ccd60 commit fa6f06f
Show file tree
Hide file tree
Showing 21 changed files with 425 additions and 326 deletions.
31 changes: 21 additions & 10 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ executors:
docker:
- image: cimg/ruby:3.1.2-browsers
environment:
- RAILS_ENV=test
- TZ: "Europe/London"
- CI: true
RAILS_ENV: test
TZ: "Europe/London"
CACHE_VERSION: v1
cloud-platform-executor:
docker:
- image: ministryofjustice/cloud-platform-tools:2.1
Expand All @@ -33,7 +33,7 @@ executors:
- image: ministryofjustice/apply-ci:latest-3.1.2
environment:
GITHUB_TEAM_NAME_SLUG: laa-apply-for-legal-aid
CI: true
CACHE_VERSION: v1
- image: cimg/postgres:10.18
- image: cimg/redis:5.0
- image: ghcr.io/ministryofjustice/hmpps-clamav:latest
Expand Down Expand Up @@ -99,24 +99,22 @@ references:
restore_gems_cache: &restore_gems_cache
restore_cache:
keys:
- v2.7-gems-cache-{{ checksum "Gemfile.lock" }}
- v2.7-gems-cache-
- v2.7-gems-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}

restore_js_packages_cache: &restore_js_packages_cache
restore_cache:
keys:
- v2.7-yarn-packages-cache-{{ checksum "yarn.lock" }}
- v2.7-yarn-packages-cache-
- v2.7-yarn-packages-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}

save_gems_cache: &save_gems_cache
save_cache:
key: v2.7-gems-cache-{{ checksum "Gemfile.lock" }}
key: v2.7-gems-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle

save_js_packages_cache: &save_js_packages_cache
save_cache:
key: v2.7-yarn-packages-cache-{{ checksum "yarn.lock" }}
key: v2.7-yarn-packages-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }}
paths:
- node_modules

Expand Down Expand Up @@ -190,6 +188,19 @@ jobs:
- *restore_js_packages_cache
- *install_js_packages
- *save_js_packages_cache
- run:
name: Install Headless Chrome dependencies
command: |
sudo apt-get install -yq \
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
- run:
name: Install Puppeteer with Chromium
command: |
yarn add [email protected]
- run:
name: Setup Code Climate test-reporter
command: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,6 @@ Brewfile.lock.json
# Ignore payload files generated by CCMS integration specs
/ccms_integration/generated/*
!/ccms_integration/generated/.keep

# Ignore Puppeteer cache
.cache
11 changes: 0 additions & 11 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,6 @@ RSpec/AnyInstance:
- 'spec/services/reports/merits_report_creator_spec.rb'
- 'spec/services/reports/mis/application_details_report_spec.rb'

# Offense count: 1
RSpec/BeforeAfterAll:
Exclude:
- 'spec/requests/providers/means_reports_spec.rb'

# Offense count: 1939
# Configuration parameters: Prefixes.
# Prefixes: when, with, without
Expand Down Expand Up @@ -426,9 +421,7 @@ RSpec/FilePath:
- 'spec/requests/providers/identify_types_of_outgoings_spec.rb'
- 'spec/requests/providers/income_summary_spec.rb'
- 'spec/requests/providers/limitations_spec.rb'
- 'spec/requests/providers/means_reports_spec.rb'
- 'spec/requests/providers/means_summaries_spec.rb'
- 'spec/requests/providers/merits_reports_spec.rb'
- 'spec/requests/providers/no_employment_incomes_spec.rb'
- 'spec/requests/providers/no_income_summaries_spec.rb'
- 'spec/requests/providers/no_outgoings_summaries_spec.rb'
Expand Down Expand Up @@ -543,7 +536,6 @@ RSpec/LetSetup:
- 'spec/requests/providers/confirm_offices_spec.rb'
- 'spec/requests/providers/income_summary_spec.rb'
- 'spec/requests/providers/legal_aid_applications_spec.rb'
- 'spec/requests/providers/merits_reports_spec.rb'
- 'spec/requests/providers/outgoings_summary_spec.rb'
- 'spec/requests/providers/proceeding_merits_task/chances_of_success_spec.rb'
- 'spec/requests/providers/submitted_applications_spec.rb'
Expand Down Expand Up @@ -707,9 +699,7 @@ RSpec/NamedSubject:
- 'spec/requests/providers/income_summary_spec.rb'
- 'spec/requests/providers/legal_aid_applications_spec.rb'
- 'spec/requests/providers/limitations_spec.rb'
- 'spec/requests/providers/means_reports_spec.rb'
- 'spec/requests/providers/means_summaries_spec.rb'
- 'spec/requests/providers/merits_reports_spec.rb'
- 'spec/requests/providers/merits_task_lists_controller_spec.rb'
- 'spec/requests/providers/no_employment_incomes_spec.rb'
- 'spec/requests/providers/no_income_summaries_spec.rb'
Expand Down Expand Up @@ -851,7 +841,6 @@ RSpec/NotToNot:
- 'spec/requests/providers/confirm_dwp_non_passported_applications_spec.rb'
- 'spec/requests/providers/employed_incomes_spec.rb'
- 'spec/requests/providers/limitations_spec.rb'
- 'spec/requests/providers/means_reports_spec.rb'
- 'spec/requests/providers/proceeding_merits_task/attempts_to_settle_controller_spec.rb'
- 'spec/requests/providers/proceeding_merits_task/chances_of_success_spec.rb'
- 'spec/requests/providers/proceeding_merits_task/linked_children_spec.rb'
Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ gem "webpacker", "~> 5", ">= 5.4.3"
gem "wdm", ">= 0.1.0" if Gem.win_platform?

# generating PDFs
gem "wicked_pdf"
gem "grover"

# DFE formbuilder
gem "govuk_design_system_formbuilder"
Expand Down
11 changes: 8 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ GEM
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
combine_pdf (1.0.22)
matrix
ruby-rc4 (>= 0.1.5)
concurrent-ruby (1.1.10)
connection_pool (2.3.0)
crack (0.4.5)
Expand Down Expand Up @@ -312,6 +315,9 @@ GEM
govuk_notify_rails (2.2.0)
notifications-ruby-client (~> 5.1)
rails (>= 4.1.0)
grover (1.1.2)
combine_pdf (~> 1.0)
nokogiri (~> 1.0)
guard (2.18.0)
formatador (>= 0.2.4)
listen (>= 2.7, < 4.0)
Expand Down Expand Up @@ -603,6 +609,7 @@ GEM
rubocop-rspec (2.15.0)
rubocop (~> 1.33)
ruby-progressbar (1.11.0)
ruby-rc4 (0.1.5)
ruby-saml (1.15.0)
nokogiri (>= 1.13.10)
rexml
Expand Down Expand Up @@ -715,8 +722,6 @@ GEM
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wicked_pdf (2.6.3)
activesupport
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.6)
Expand Down Expand Up @@ -754,6 +759,7 @@ DEPENDENCIES
govuk-components
govuk_design_system_formbuilder
govuk_notify_rails (~> 2.2.0)
grover
guard-cucumber
guard-livereload
guard-rspec
Expand Down Expand Up @@ -819,7 +825,6 @@ DEPENDENCIES
webmock
webpacker (~> 5, >= 5.4.3)
webrick
wicked_pdf

RUBY VERSION
ruby 3.1.2p20
Expand Down
11 changes: 8 additions & 3 deletions app/controllers/providers/means_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ class MeansReportsController < ProviderBaseController
def show
@cfe_result = @legal_aid_application.cfe_result
@manual_review_determiner = CCMS::ManualReviewDeterminer.new(@legal_aid_application)
render pdf: "Means report",
layout: "pdf",
show_as_html: params.key?(:debug)

if params.key?(:debug)
render "show", layout: "pdf"
else
html = render_to_string "show", layout: "pdf"
pdf = Grover.new(html).to_pdf
send_data pdf, filename: "means_report.pdf", type: "application/pdf", disposition: "inline"
end
end
end
end
8 changes: 5 additions & 3 deletions app/controllers/providers/merits_reports_controller.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
module Providers
class MeritsReportsController < ProviderBaseController
authorize_with_policy_method :show_submitted_application?

def show
render pdf: "Merit report",
layout: "pdf",
show_as_html: params.key?(:debug)
html = render_to_string "show", layout: "pdf"
pdf = Grover.new(html).to_pdf

send_data pdf, filename: "merits_report.pdf", type: "application/pdf", disposition: "inline"
end
end
end
6 changes: 0 additions & 6 deletions app/helpers/pdf_helper.rb

This file was deleted.

2 changes: 1 addition & 1 deletion app/services/reports/base_report_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def initialize(legal_aid_application)
private

def pdf_report
WickedPdf.new.pdf_from_string(html_report)
Grover.new(html_report).to_pdf
end

def ensure_case_ccms_reference_exists
Expand Down
16 changes: 0 additions & 16 deletions app/views/layouts/_govuk_base64_fonts.html.erb

This file was deleted.

3 changes: 1 addition & 2 deletions app/views/layouts/pdf.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
<head>
<meta charset="utf-8">
<title><%= t("layouts.application.header.title") %></title>
<%= apply_webpacker_stylesheet_pack_tag("styles") %>
<%= render "layouts/govuk_base64_fonts" %>
<%= stylesheet_pack_tag "styles", media: "all" %>
</head>

<body class="govuk-template__body" style="padding-left: 3em; padding-top: 3em;">
Expand Down
24 changes: 24 additions & 0 deletions config/initializers/grover.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
Grover.configure do |config|
protocol = if Rails.env.development?
"http://"
else
"https://"
end

config.options = {
format: "A4",
emulate_media: "print",
prefer_css_page_size: true,
bypass_csp: true,
cache: false,
wait_until: "networkidle2",
display_url: protocol + ENV.fetch("HOST", "localhost:3002"),
margin: {
top: "10mm",
bottom: "10mm",
left: "10mm",
right: "20mm",
},
launch_args: ["--no-sandbox"],
}
end
13 changes: 0 additions & 13 deletions config/initializers/wicked_pdf.rb

This file was deleted.

19 changes: 19 additions & 0 deletions docker/apply_base.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ RUN apk --no-cache add --virtual build-dependencies \
py3-pip
RUN pip3 install awscli

# Install Chromium and Puppeteer for PDF generation
# Installs latest Chromium package available on Alpine (Chromium 108)
RUN apk add --no-cache \
chromium \
nss \
freetype \
harfbuzz \
ca-certificates \
ttf-freefont \
nodejs \
yarn

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Install latest version of Puppeteer that works with Chromium 108
RUN yarn add [email protected]

# Install kubectl
RUN curl -Lo /usr/local/bin/kubectl \
--retry 3 \
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"stylelint": "^14.16.1",
"stylelint-config-gds": "^0.2.0",
"stylelint-order": "^5.0.0",
"webpack-dev-server": "^4.11.1"
"webpack-dev-server": "^4.11.1",
"puppeteer": "^19.2.0"
},
"resolutions": {
"acorn": "^7.1.1",
Expand Down
8 changes: 8 additions & 0 deletions puppeteer.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const { join } = require('path')

/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {
cacheDirectory: join(__dirname, '.cache', 'puppeteer')
}
Loading

0 comments on commit fa6f06f

Please sign in to comment.