From 3a41bd5b20174fc835379a2456a3bc9279a44246 Mon Sep 17 00:00:00 2001
From: starswan <github@starswan.com>
Date: Mon, 25 Nov 2024 13:37:11 +0000
Subject: [PATCH 1/2] Cache PDF on job submission, and download it if available

---
 Gemfile                                       |  1 +
 Gemfile.lock                                  | 12 ++++++---
 .../vacancies/job_applications_controller.rb  | 26 +++++++++++++------
 app/jobs/make_job_application_pdf_job.rb      | 13 ++++++++++
 app/models/job_application.rb                 |  3 +++
 config/application.rb                         |  2 ++
 lib/tasks/backfill_application_pdfs.rake      | 11 ++++++++
 .../sidekiq_scheduled_jobs_spec.rb            |  1 +
 .../jobs/make_job_application_pdf_job_spec.rb | 10 +++++++
 .../vacancies/job_applications_spec.rb        | 20 ++++++++++++++
 10 files changed, 88 insertions(+), 11 deletions(-)
 create mode 100644 app/jobs/make_job_application_pdf_job.rb
 create mode 100644 lib/tasks/backfill_application_pdfs.rake
 create mode 100644 spec/jobs/make_job_application_pdf_job_spec.rb

diff --git a/Gemfile b/Gemfile
index 2cb08b05dd..c5d36549da 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,6 +38,7 @@ gem "govuk-components", "~> 5.7.1"
 gem "govuk_design_system_formbuilder", "~> 5.7.1"
 gem "high_voltage"
 gem "httparty"
+gem "image_processing", "~> 1.2"
 gem "ipaddr"
 gem "jbuilder"
 gem "jwt"
diff --git a/Gemfile.lock b/Gemfile.lock
index 690b2e18fe..625a60f3c8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -226,8 +226,7 @@ GEM
     faraday-net_http (3.3.0)
       net-http
     fastimage (2.3.1)
-    ffi (1.17.0-arm64-darwin)
-    ffi (1.17.0-x86_64-linux-gnu)
+    ffi (1.16.3)
     friendly_id (5.5.1)
       activerecord (>= 4.0.0)
     front_matter_parser (1.0.1)
@@ -296,6 +295,9 @@ GEM
     httpclient (2.8.3)
     i18n (1.14.6)
       concurrent-ruby (~> 1.0)
+    image_processing (1.13.0)
+      mini_magick (>= 4.9.5, < 5)
+      ruby-vips (>= 2.0.17, < 3)
     inflection (1.0.0)
     io-console (0.7.2)
     ipaddr (1.2.7)
@@ -353,7 +355,7 @@ GEM
     mimemagic (0.4.3)
       nokogiri (~> 1)
       rake
-    mini_magick (5.0.1)
+    mini_magick (4.13.2)
     mini_mime (1.1.5)
     minitest (5.25.1)
     multi_json (1.15.0)
@@ -618,6 +620,9 @@ GEM
       rubocop-rspec (~> 3, >= 3.0.1)
     ruby-progressbar (1.13.0)
     ruby-rc4 (0.1.5)
+    ruby-vips (2.2.2)
+      ffi (~> 1.12)
+      logger
     rubyzip (2.3.2)
     sanitize (6.1.3)
       crass (~> 1.0.2)
@@ -812,6 +817,7 @@ DEPENDENCIES
   govuk_design_system_formbuilder (~> 5.7.1)
   high_voltage
   httparty
+  image_processing (~> 1.2)
   ipaddr
   jbuilder
   jsbundling-rails
diff --git a/app/controllers/publishers/vacancies/job_applications_controller.rb b/app/controllers/publishers/vacancies/job_applications_controller.rb
index 6395b73888..3e31cae89c 100644
--- a/app/controllers/publishers/vacancies/job_applications_controller.rb
+++ b/app/controllers/publishers/vacancies/job_applications_controller.rb
@@ -23,14 +23,24 @@ def show
   end
 
   def download_pdf
-    pdf = JobApplicationPdfGenerator.new(job_application, vacancy).generate
-
-    send_data(
-      pdf.render,
-      filename: "job_application_#{job_application.id}.pdf",
-      type: "application/pdf",
-      disposition: "inline",
-    )
+    if job_application.pdf_version.attached?
+      send_data(
+        job_application.pdf_version.attachment.download,
+        filename: "job_application_#{job_application.id}.pdf",
+        type: "application/pdf",
+        disposition: "inline",
+        )
+    else
+      pdf = JobApplicationPdfGenerator.new(job_application, vacancy).generate
+      pdf_data = pdf.render
+
+      send_data(
+        pdf_data,
+        filename: "job_application_#{job_application.id}.pdf",
+        type: "application/pdf",
+        disposition: "inline",
+        )
+    end
   end
 
   def update_status
diff --git a/app/jobs/make_job_application_pdf_job.rb b/app/jobs/make_job_application_pdf_job.rb
new file mode 100644
index 0000000000..e9fd579072
--- /dev/null
+++ b/app/jobs/make_job_application_pdf_job.rb
@@ -0,0 +1,13 @@
+class MakeJobApplicationPdfJob < ApplicationJob
+  queue_as :low
+
+  def perform(job_application)
+    pdf = JobApplicationPdfGenerator.new(job_application, job_application.vacancy).generate
+    pdf_data = pdf.render
+
+    job_application.pdf_version.attach(io: StringIO.open(pdf_data),
+                                       filename: "job_application_#{job_application.id}.pdf",
+                                       identify: false,
+                                       content_type: "application/pdf")
+  end
+end
diff --git a/app/models/job_application.rb b/app/models/job_application.rb
index 1e2ccdeb58..04026936d5 100644
--- a/app/models/job_application.rb
+++ b/app/models/job_application.rb
@@ -58,6 +58,8 @@ class JobApplication < ApplicationRecord
 
   has_many :feedbacks, dependent: :destroy, inverse_of: :job_application
 
+  has_one_attached :pdf_version, service: :amazon_s3_documents
+
   has_noticed_notifications
 
   scope :submitted_yesterday, -> { submitted.where("DATE(submitted_at) = ?", Date.yesterday) }
@@ -80,6 +82,7 @@ def submit!
     submitted!
     Publishers::JobApplicationReceivedNotifier.with(vacancy: vacancy, job_application: self).deliver(vacancy.publisher)
     Jobseekers::JobApplicationMailer.application_submitted(self).deliver_later
+    MakeJobApplicationPdfJob.perform_later self
   end
 
   def for_a_teaching_role?
diff --git a/config/application.rb b/config/application.rb
index bc261bdbe2..e3057f02cb 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -57,6 +57,8 @@ class Application < Rails::Application
 
     config.active_storage.routes_prefix = "/attachments"
     config.active_storage.resolve_model_to_route = :rails_storage_proxy
+    # avoid default of :vips for now - we already have mini_magick
+    config.active_storage.variant_processor = :mini_magick
 
     config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info").to_sym
 
diff --git a/lib/tasks/backfill_application_pdfs.rake b/lib/tasks/backfill_application_pdfs.rake
new file mode 100644
index 0000000000..d1c6970524
--- /dev/null
+++ b/lib/tasks/backfill_application_pdfs.rake
@@ -0,0 +1,11 @@
+namespace :vacancy do
+  desc "backfill all submitted applications to have a PDF version"
+
+  task backfill_application_pdfs: :environment do
+    Vacancy.expires_within_data_access_period.find_each do |vacancy|
+      vacancy.job_applications.after_submission.reject { |ja| ja.pdf_version.attached? }.each do |job_application|
+        MakeJobApplicationPdfJob.perform_later job_application
+      end
+    end
+  end
+end
diff --git a/spec/configuration/sidekiq_scheduled_jobs_spec.rb b/spec/configuration/sidekiq_scheduled_jobs_spec.rb
index 4354e7a287..0d93d1f003 100644
--- a/spec/configuration/sidekiq_scheduled_jobs_spec.rb
+++ b/spec/configuration/sidekiq_scheduled_jobs_spec.rb
@@ -26,6 +26,7 @@
       SetOrganisationSlugsJob
       SetOrganisationSlugsOfBatchJob
       ImportFromVacancySourceJob
+      MakeJobApplicationPdfJob
     ]
   end
 
diff --git a/spec/jobs/make_job_application_pdf_job_spec.rb b/spec/jobs/make_job_application_pdf_job_spec.rb
new file mode 100644
index 0000000000..f52dfe3fc1
--- /dev/null
+++ b/spec/jobs/make_job_application_pdf_job_spec.rb
@@ -0,0 +1,10 @@
+require "rails_helper"
+
+RSpec.describe MakeJobApplicationPdfJob do
+  let(:job_application) { create(:job_application, :status_submitted) }
+
+  it "produces a PDF when called" do
+    described_class.perform_now job_application
+    expect(job_application.pdf_version).not_to be_nil
+  end
+end
diff --git a/spec/requests/publishers/vacancies/job_applications_spec.rb b/spec/requests/publishers/vacancies/job_applications_spec.rb
index 61bce64e85..fbdc62dd77 100644
--- a/spec/requests/publishers/vacancies/job_applications_spec.rb
+++ b/spec/requests/publishers/vacancies/job_applications_spec.rb
@@ -107,6 +107,26 @@
     end
   end
 
+  describe "download pdf" do
+    context "without cache" do
+      it "allows the PDF to be downloaded" do
+        get(organisation_job_job_application_download_pdf_path(vacancy.id, job_application.id))
+        expect(response.body).to satisfy { |body| body.size > 128000 }
+      end
+    end
+
+    context "with cache" do
+      before do
+        MakeJobApplicationPdfJob.perform_now job_application
+      end
+
+      it "allows the PDF to be downloaded" do
+        get(organisation_job_job_application_download_pdf_path(vacancy.id, job_application.id))
+        expect(response.body).to satisfy { |body| body.size > 128000 }
+      end
+    end
+  end
+
   describe "GET #index" do
     context "when the vacancy does not belong to the current organisation" do
       let(:vacancy) { create(:vacancy, :published, organisations: [build(:school)]) }

From 91074e6e35ac0ba2361485f4e1109ff25d1d144a Mon Sep 17 00:00:00 2001
From: starswan <github@starswan.com>
Date: Mon, 25 Nov 2024 14:03:41 +0000
Subject: [PATCH 2/2] linter

---
 .../publishers/vacancies/job_applications_controller.rb       | 4 ++--
 spec/requests/publishers/vacancies/job_applications_spec.rb   | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/app/controllers/publishers/vacancies/job_applications_controller.rb b/app/controllers/publishers/vacancies/job_applications_controller.rb
index 3e31cae89c..9924ea0fb4 100644
--- a/app/controllers/publishers/vacancies/job_applications_controller.rb
+++ b/app/controllers/publishers/vacancies/job_applications_controller.rb
@@ -29,7 +29,7 @@ def download_pdf
         filename: "job_application_#{job_application.id}.pdf",
         type: "application/pdf",
         disposition: "inline",
-        )
+      )
     else
       pdf = JobApplicationPdfGenerator.new(job_application, vacancy).generate
       pdf_data = pdf.render
@@ -39,7 +39,7 @@ def download_pdf
         filename: "job_application_#{job_application.id}.pdf",
         type: "application/pdf",
         disposition: "inline",
-        )
+      )
     end
   end
 
diff --git a/spec/requests/publishers/vacancies/job_applications_spec.rb b/spec/requests/publishers/vacancies/job_applications_spec.rb
index fbdc62dd77..04eac0d157 100644
--- a/spec/requests/publishers/vacancies/job_applications_spec.rb
+++ b/spec/requests/publishers/vacancies/job_applications_spec.rb
@@ -111,7 +111,7 @@
     context "without cache" do
       it "allows the PDF to be downloaded" do
         get(organisation_job_job_application_download_pdf_path(vacancy.id, job_application.id))
-        expect(response.body).to satisfy { |body| body.size > 128000 }
+        expect(response.body).to(satisfy { |body| body.size > 128_000 })
       end
     end
 
@@ -122,7 +122,7 @@
 
       it "allows the PDF to be downloaded" do
         get(organisation_job_job_application_download_pdf_path(vacancy.id, job_application.id))
-        expect(response.body).to satisfy { |body| body.size > 128000 }
+        expect(response.body).to(satisfy { |body| body.size > 128_000 })
       end
     end
   end