From df6d830bb6cb74316f0219612a5146da679185db Mon Sep 17 00:00:00 2001 From: starswan Date: Wed, 9 Oct 2024 15:41:44 +0100 Subject: [PATCH] spike downloads with checkboxes --- Gemfile | 1 + Gemfile.lock | 1 + app/assets/javascript/application.js | 10 ++- app/assets/stylesheets/application.scss | 3 + .../vacancies/job_applications_controller.rb | 73 +++++++++++++++++++ app/helpers/pdf_helper.rb | 11 ++- app/models/vacancy.rb | 8 +- app/services/job_application_pdf_generator.rb | 15 ++++ .../job_applications/index.html.slim | 64 +++++++++++----- config/environments/development.rb | 2 + config/routes.rb | 1 + package.json | 2 + 12 files changed, 165 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index c8568a7969..c82450b8db 100644 --- a/Gemfile +++ b/Gemfile @@ -67,6 +67,7 @@ gem "rails_semantic_logger" gem "recaptcha" gem "redis" gem "rgeo-geojson" +gem "rubyzip" gem "sanitize" gem "sentry-rails" gem "sentry-ruby" diff --git a/Gemfile.lock b/Gemfile.lock index 82cc96d4f3..27f7be0f8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -841,6 +841,7 @@ DEPENDENCIES rubocop-govuk rubocop-performance rubocop-rspec_rails + rubyzip sanitize selenium-webdriver sentry-rails diff --git a/app/assets/javascript/application.js b/app/assets/javascript/application.js index 160b24a763..bf7bef2435 100644 --- a/app/assets/javascript/application.js +++ b/app/assets/javascript/application.js @@ -3,7 +3,10 @@ import * as Sentry from '@sentry/browser'; import 'core-js/modules/es.weak-map'; import 'core-js/modules/es.weak-set'; import '@stimulus/polyfills'; -import { initAll } from 'govuk-frontend'; +import { initAll as govukInit } from 'govuk-frontend'; +import $ from 'jquery'; +import { initAll as mojInit } from '@ministryofjustice/frontend'; + import { Application } from '@hotwired/stimulus'; import Rails from 'rails-ujs'; @@ -61,4 +64,7 @@ application.register('tracked-link', TrackedLinkController); application.register('utils', UtilsController); Rails.start(); -initAll(); +govukInit(); +window.$ = $; +mojInit(); +// window.MOJFrontend = MOJFrontend; diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index c316052900..a7c4e34003 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -9,6 +9,9 @@ $govuk-assets-path: "/"; @import 'leaflet/dist/leaflet'; @import 'leaflet.markercluster/dist/MarkerCluster'; @import 'leaflet-gesture-handling/dist/leaflet-gesture-handling'; +// oh dear this file is broken as it tries to import from node_modules +//@import "@ministryofjustice/frontend/moj/all"; +@import "@ministryofjustice/frontend/moj/components/multi-select/multi-select"; @import 'base/global'; @import 'base/guide_card'; diff --git a/app/controllers/publishers/vacancies/job_applications_controller.rb b/app/controllers/publishers/vacancies/job_applications_controller.rb index 6395b73888..b4d7ff3c3e 100644 --- a/app/controllers/publishers/vacancies/job_applications_controller.rb +++ b/app/controllers/publishers/vacancies/job_applications_controller.rb @@ -2,6 +2,8 @@ class Publishers::Vacancies::JobApplicationsController < Publishers::Vacancies:: include Jobseekers::QualificationFormConcerns include DatesHelper + include ActionController::Live + helper_method :employments, :form, :job_applications, :qualification_form_param_key, :sort, :sorted_job_applications def reject @@ -42,8 +44,79 @@ def update_status redirect_to organisation_job_job_applications_path(vacancy.id), success: t(".#{status}", name: job_application.name) end + require "zip" + + def download_selected + # This eliminates all the N+1 issues, but PDF generation still takes ~1.5 seconds per application + # writing a text string to the PDF seems to take 200ms, and 'closing' the document ~500ms + downloads = Vacancy + .includes(:organisations, :publisher_organisation) + .includes(job_applications: [:qualifications, :employments, :training_and_cpds, :references, { jobseeker: :jobseeker_profile }]) + .find(vacancy.id) + .job_applications.select { |job_application| params[:applications].include?(job_application.id) } + + stringio = Zip::OutputStream.write_buffer do |zio| + downloads.each do |job_application| + zio.put_next_entry "job_application_#{job_application.id}.pdf" + logger.debug "generate #{job_application.id}.pdf" + pdf = JobApplicationPdfGenerator.new(job_application, vacancy).generate + logger.debug "render #{job_application.id}.pdf" + zio.write pdf.render + logger.debug "finished #{job_application.id}.pdf" + end + end + send_data( + stringio.string, + filename: "applications_#{vacancy.id}.zip", + type: "application/zip", + disposition: "inline", + ) + # This would seem to do streaming, but the User experience seems very similar + # and also it doesn't produce a valid Zip file + # send_stream( + # filename: "applications_#{vacancy.id}.zip", + # type: "application/zip", + # disposition: "inline", + # ) do |stream| + # io = StringIO.new + # pos = 0 + # Zip::OutputStream.write_buffer(io) do |zio| + # downloads.each do |job_application| + # zio.put_next_entry "job_application_#{job_application.id}.pdf" + # zio.write JobApplicationPdfGenerator.new(job_application, vacancy).generate.render + # + # io.seek pos + # stream.write io.read + # pos = io.size + # io.seek pos + # end + # end + # io.seek pos + # stream.write io.read + # end + end + private + def generate_zip(downloads) + Enumerator.new { |yielder| + io = StringIO.new + pos = 0 + Zip::OutputStream.write_buffer(io) do |zio| + downloads.each do |job_application| + zio.put_next_entry "job_application_#{job_application.id}.pdf" + zio.write JobApplicationPdfGenerator.new(job_application, vacancy).generate.render + + io.seek pos + yielder << io.read + pos = io.size + end + end + io.seek pos + yielder << io.read + }.lazy + end + def job_applications @job_applications ||= vacancy.job_applications.not_draft end diff --git a/app/helpers/pdf_helper.rb b/app/helpers/pdf_helper.rb index 8999e6af01..f42ba7fd2e 100644 --- a/app/helpers/pdf_helper.rb +++ b/app/helpers/pdf_helper.rb @@ -25,14 +25,17 @@ def add_section_title(pdf, title) end def render_table(pdf, data) - pdf.table(data, cell_style: { borders: [] }) do - cells.padding = 12 - cells.borders = [] + pdf.table(data, cell_style: { borders: [], padding: 12 }) do columns(0).font_style = :bold columns(0).align = :left columns(1).align = :left columns(0).width = 150 end + # data.each do |description, value| + # pdf.text description, style: :bold + # pdf.draw_text value, at: [150, pdf.cursor] + # pdf.move_down 30 + # end end def add_headers(pdf) @@ -153,7 +156,7 @@ def add_employment_history(pdf) if job_application.employments.none? render_no_employment_message(pdf) else - job_application.employments.sort_by { |r| r[:started_on] }.reverse.each_with_index do |employment, _index| + job_application.employments.sort_by { |r| r[:started_on] }.reverse_each do |employment| render_employment_entry(pdf, employment) render_employment_break(pdf, employment) render_unexplained_gap(pdf, employment) diff --git a/app/models/vacancy.rb b/app/models/vacancy.rb index 978ff0f439..d6181c0c5a 100644 --- a/app/models/vacancy.rb +++ b/app/models/vacancy.rb @@ -132,9 +132,11 @@ def external? end def organisation - return organisations.first if organisations.one? - - organisations.find(&:trust?) || publisher_organisation || organisations.first&.school_groups&.first + if organisations.size == 1 + organisations.first + else + organisations.detect(&:trust?) || publisher_organisation || organisations.first&.school_groups&.first + end end def location diff --git a/app/services/job_application_pdf_generator.rb b/app/services/job_application_pdf_generator.rb index 572b5e7140..423e367688 100644 --- a/app/services/job_application_pdf_generator.rb +++ b/app/services/job_application_pdf_generator.rb @@ -7,21 +7,36 @@ def initialize(job_application, vacancy) end def generate + logger = Rails.logger Prawn::Document.new do |pdf| + logger.debug("start - update_font_family") update_font_family(pdf) + logger.debug("add_image_to_first_page") add_image_to_first_page(pdf) + logger.debug("add_headers") add_headers(pdf) pdf.stroke_horizontal_rule + logger.debug("add_personal_details") add_personal_details(pdf) + logger.debug("add_professional_status") add_professional_status(pdf) + logger.debug("add_qualifications") add_qualifications(pdf) + logger.debug("add_training_and_cpds") add_training_and_cpds(pdf) + logger.debug("add_employment_history") add_employment_history(pdf) + logger.debug("add_personal_statement") add_personal_statement(pdf) + logger.debug("add_references") add_references(pdf) + logger.debug("add_ask_for_support") add_ask_for_support(pdf) + logger.debug("add_declarations") add_declarations(pdf) + logger.debug("add_footers") add_footers(pdf) + logger.debug("done") end end diff --git a/app/views/publishers/vacancies/job_applications/index.html.slim b/app/views/publishers/vacancies/job_applications/index.html.slim index 8a73d53f73..b4aa636953 100644 --- a/app/views/publishers/vacancies/job_applications/index.html.slim +++ b/app/views/publishers/vacancies/job_applications/index.html.slim @@ -10,26 +10,56 @@ .govuk-grid-row .govuk-grid-column-full - if job_applications.any? && vacancy.within_data_access_period? - = govuk_summary_list do |summary_list| - - sorted_job_applications.each do |application| - - summary_list.with_row classes: "application-#{application.status}" do |row| - - row.with_key do - = job_application_view_applicant(vacancy, application) - - - row.with_value do - = publisher_job_application_status_tag(application.status) - dl - dt = "#{t('.received')}:" - dd = application.submitted_at.strftime("%d %B %Y at %H:%M") - - - if application.status.in?(%w[reviewed shortlisted submitted]) - - if application.reviewed? || application.submitted? - - row.with_action text: t("buttons.shortlist"), href: organisation_job_job_application_shortlist_path(vacancy.id, application.id), visually_hidden_text: " #{application.name}" - - row.with_action text: t("buttons.reject"), href: organisation_job_job_application_reject_path(vacancy.id, application.id), visually_hidden_text: " #{application.name}" + = form_with url: download_selected_organisation_job_job_applications_path(vacancy.id) do |f| + div data-module="moj-multi-select" data-multi-select-checkbox="#select-everything" + = govuk_summary_list(html_attributes: { id: "select-everything"}) do |summary_list| + - summary_list.with_row do |row| + - row.with_key do + = f.govuk_check_box :ignore, + :ignore, + class: "moj-multi-select__checkbox", + label: { text: "Name" } + + / - lunch_options = [ \ + / OpenStruct.new(id: 1, name: 'Salad', description: 'Lettuce, tomato and cucumber'), \ + / OpenStruct.new(id: 2, name: 'Jacket potato', description: 'With cheese and baked beans') \ + / ] + / = f.govuk_collection_check_boxes :wednesday_lunch_ids, + / lunch_options, + / :id, + / :name, + / :description, + / small: true, + / legend: { text: "What would you like for lunch on Wednesday?" } + + - sorted_job_applications.each do |application| + - summary_list.with_row classes: "application-#{application.status}" do |row| + + - row.with_key do + div + = f.govuk_check_box(:applications, + application.id.to_sym, + class: "moj-multi-select__checkbox", + label: { text: application.name }) + + / = job_application_view_applicant(vacancy, application) + + - row.with_value do + = publisher_job_application_status_tag(application.status) + dl + dt = "#{t('.received')}:" + dd = application.submitted_at.strftime("%d %B %Y at %H:%M") + + - if application.status.in?(%w[reviewed shortlisted submitted]) + - if application.reviewed? || application.submitted? + - row.with_action text: t("buttons.shortlist"), href: organisation_job_job_application_shortlist_path(vacancy.id, application.id), visually_hidden_text: " #{application.name}" + - row.with_action text: t("buttons.reject"), href: organisation_job_job_application_reject_path(vacancy.id, application.id), visually_hidden_text: " #{application.name}" + + = f.govuk_submit "Download Selected" - elsif !vacancy.within_data_access_period? = govuk_inset_text do p = t(".expired_more_than_year") - else - = render EmptySectionComponent.new title: t(".no_applicants") + = render EmptySectionComponent.new title: t(".no_applicants") diff --git a/config/environments/development.rb b/config/environments/development.rb index 0fb120c847..313bf1e6b7 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -90,4 +90,6 @@ config.i18n.raise config.log_file_size = 100.megabytes + + config.log_level = :debug end diff --git a/config/routes.rb b/config/routes.rb index 923c4e5b26..e38e7e3259 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -371,6 +371,7 @@ get :reject get :withdrawn post :update_status + post :download_selected, on: :collection end end end diff --git a/package.json b/package.json index d58d1a075b..020003a35a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "dependencies": { "@hotwired/stimulus": "^3.2.2", + "@ministryofjustice/frontend": "^2.2.4", "@sentry/browser": "8.35.0", "@stimulus/polyfills": "^2.0.0", "accessible-autocomplete": "^3.0.1", @@ -21,6 +22,7 @@ "dfe-frontend": "^2.0.1", "dompurify": "^3.1.7", "govuk-frontend": "^5.7.1", + "jquery": "^3.6.0", "leaflet": "^1.9.4", "leaflet-gesture-handling": "^1.2.2", "leaflet.markercluster": "^1.5.3",