Browse Source
* feat: add schemes and locations download links and pages * feat: update current path helper * feat: update tests for different user visibility levels * feat: update search caption tests * refactor: lint tests * refactor: lint tests * git: revert unintentional inclusion * feat: update tests * refactor: lint * feat: DRY up routing * refactor: lint * feat: add csv confirmation view * feat: add scheme csv service * feat: rename * feat: update csv service * feat: update csv service * feat: update controller and rename view * feat: update view * refactor: lint * feat: show correct headers in csv * feat: add locations and combined csv behaviour * feat: remove redundant user instance variable * feat: add scheme csv service spec * feat: add scheme email csv job tests * feat: update filters in spec * refactor: move scheme_email_csv_job_spec.rb * feat: update spec * refactor: remove blank line * feat: add nowrap to all download links * feat: update org schemes controller with org schemes (and rename for clarity) * feat: update link indentation and spec * feat: only include location LA name, and rename to location_local_authority * feat: update seed locations with westminster local authorities to avoid similar confusion to some that arose in PO review * feat: display multiple active periods on a single line * feat: display multiple active periods on a single line * feat: update line spacing in search captions * feat: replace 2/3 with full column in download page * feat: move scheme alphabeticising into manager * feat: update tests now search/filterless copy has changed * refactor: lint * refactor: lint * refactor: lint * feat: add filter alphabeticising test * feat: correct spacingpull/2090/head
natdeanlewissoftwire
1 year ago
committed by
GitHub
35 changed files with 748 additions and 54 deletions
@ -0,0 +1,29 @@ |
|||||||
|
class SchemeEmailCsvJob < ApplicationJob |
||||||
|
queue_as :default |
||||||
|
|
||||||
|
BYTE_ORDER_MARK = "\uFEFF".freeze # Required to ensure Excel always reads CSV as UTF-8 |
||||||
|
|
||||||
|
EXPIRATION_TIME = 24.hours.to_i |
||||||
|
|
||||||
|
def perform(user, search_term = nil, filters = {}, all_orgs = false, organisation = nil, download_type = "combined") # rubocop:disable Style/OptionalBooleanParameter - sidekiq can't serialise named params |
||||||
|
unfiltered_schemes = organisation.present? && user.support? ? Scheme.where(owning_organisation_id: organisation.id) : user.schemes |
||||||
|
filtered_schemes = FilterManager.filter_schemes(unfiltered_schemes, search_term, filters, all_orgs, user) |
||||||
|
csv_string = Csv::SchemeCsvService.new(download_type:).prepare_csv(filtered_schemes) |
||||||
|
|
||||||
|
case download_type |
||||||
|
when "schemes" |
||||||
|
filename = "#{['schemes', organisation&.name, Time.zone.now].compact.join('-')}.csv" |
||||||
|
when "locations" |
||||||
|
filename = "#{['locations', organisation&.name, Time.zone.now].compact.join('-')}.csv" |
||||||
|
when "combined" |
||||||
|
filename = "#{['schemes-and-locations', organisation&.name, Time.zone.now].compact.join('-')}.csv" |
||||||
|
end |
||||||
|
|
||||||
|
storage_service = Storage::S3Service.new(Configuration::EnvConfigurationService.new, ENV["CSV_DOWNLOAD_PAAS_INSTANCE"]) |
||||||
|
storage_service.write_file(filename, BYTE_ORDER_MARK + csv_string) |
||||||
|
|
||||||
|
url = storage_service.get_presigned_url(filename, EXPIRATION_TIME) |
||||||
|
|
||||||
|
CsvDownloadMailer.new.send_csv_download_mail(user, url, EXPIRATION_TIME) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,108 @@ |
|||||||
|
module Csv |
||||||
|
class SchemeCsvService |
||||||
|
include SchemesHelper |
||||||
|
include LocationsHelper |
||||||
|
|
||||||
|
def initialize(download_type:) |
||||||
|
@download_type = download_type |
||||||
|
end |
||||||
|
|
||||||
|
def prepare_csv(schemes) |
||||||
|
CSV.generate(headers: true) do |csv| |
||||||
|
csv << attributes |
||||||
|
schemes.find_each do |scheme| |
||||||
|
if @download_type == "schemes" |
||||||
|
csv << scheme_attributes.map { |attribute| scheme_value(attribute, scheme) } |
||||||
|
else |
||||||
|
scheme.locations.each do |location| |
||||||
|
case @download_type |
||||||
|
when "locations" |
||||||
|
csv << [scheme.id_to_display] + location_attributes.map { |attribute| location_value(attribute, location) } |
||||||
|
when "combined" |
||||||
|
csv << scheme_attributes.map { |attribute| scheme_value(attribute, scheme) } + location_attributes.map { |attribute| location_value(attribute, location) } |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
private |
||||||
|
|
||||||
|
SCHEME_FIELD_FROM_ATTRIBUTE = { |
||||||
|
"scheme_code" => "id_to_display", |
||||||
|
"scheme_service_name" => "service_name", |
||||||
|
"scheme_status" => "status", |
||||||
|
"scheme_sensitive" => "sensitive", |
||||||
|
"scheme_registered_under_care_act" => "registered_under_care_act", |
||||||
|
"scheme_support_services_provided_by" => "arrangement_type", |
||||||
|
"scheme_primary_client_group" => "primary_client_group", |
||||||
|
"scheme_has_other_client_group" => "has_other_client_group", |
||||||
|
"scheme_secondary_client_group" => "secondary_client_group", |
||||||
|
"scheme_support_type" => "support_type", |
||||||
|
"scheme_intended_stay" => "intended_stay", |
||||||
|
"scheme_created_at" => "created_at", |
||||||
|
}.freeze |
||||||
|
|
||||||
|
LOCATION_FIELD_FROM_ATTRIBUTE = { |
||||||
|
"location_code" => "id", |
||||||
|
"location_postcode" => "postcode", |
||||||
|
"location_name" => "name", |
||||||
|
"location_status" => "status", |
||||||
|
"location_local_authority" => "location_admin_district", |
||||||
|
"location_units" => "units", |
||||||
|
"location_type_of_unit" => "type_of_unit", |
||||||
|
"location_mobility_type" => "mobility_type", |
||||||
|
}.freeze |
||||||
|
|
||||||
|
CUSTOM_CALL_CHAINS = { |
||||||
|
scheme_owning_organisation_name: %i[owning_organisation name], |
||||||
|
}.freeze |
||||||
|
|
||||||
|
SYSTEM_DATE_FIELDS = %w[ |
||||||
|
created_at |
||||||
|
].freeze |
||||||
|
|
||||||
|
def scheme_value(attribute, scheme) |
||||||
|
attribute = SCHEME_FIELD_FROM_ATTRIBUTE.fetch(attribute, attribute) |
||||||
|
if attribute == "scheme_active_dates" |
||||||
|
scheme_availability(scheme).gsub("\n", ", ").to_s |
||||||
|
elsif CUSTOM_CALL_CHAINS.key? attribute.to_sym |
||||||
|
call_chain = CUSTOM_CALL_CHAINS[attribute.to_sym] |
||||||
|
call_chain.reduce(scheme) { |object, next_call| object&.public_send(next_call) } |
||||||
|
elsif SYSTEM_DATE_FIELDS.include? attribute |
||||||
|
scheme.public_send(attribute)&.iso8601 |
||||||
|
else |
||||||
|
scheme.public_send(attribute) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def location_value(attribute, location) |
||||||
|
attribute = LOCATION_FIELD_FROM_ATTRIBUTE.fetch(attribute, attribute) |
||||||
|
if attribute == "location_active_dates" |
||||||
|
location_availability(location).gsub("\n", ", ").to_s |
||||||
|
else |
||||||
|
location.public_send(attribute) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def scheme_attributes |
||||||
|
%w[scheme_code scheme_service_name scheme_status scheme_sensitive scheme_type scheme_registered_under_care_act scheme_owning_organisation_name scheme_support_services_provided_by scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at scheme_active_dates] |
||||||
|
end |
||||||
|
|
||||||
|
def location_attributes |
||||||
|
%w[location_code location_postcode location_name location_status location_local_authority location_units location_type_of_unit location_mobility_type location_active_dates] |
||||||
|
end |
||||||
|
|
||||||
|
def attributes |
||||||
|
case @download_type |
||||||
|
when "schemes" |
||||||
|
scheme_attributes |
||||||
|
when "locations" |
||||||
|
%w[scheme_code] + location_attributes |
||||||
|
when "combined" |
||||||
|
scheme_attributes + location_attributes |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
<% content_for :title, "We’re sending you an email" %> |
||||||
|
<div class="govuk-grid-row"> |
||||||
|
<div class="govuk-grid-column-two-thirds"> |
||||||
|
<%= govuk_panel(title_text: "We’re sending you an email") %> |
||||||
|
|
||||||
|
<p class="govuk-body">It should arrive in a few minutes, but it could take longer.</p> |
||||||
|
|
||||||
|
<h2 class="govuk-heading-m">What happens next</h2> |
||||||
|
<p class="govuk-body">Open your email inbox and click the link to download your CSV file.</p> |
||||||
|
|
||||||
|
<p class="govuk-body"> |
||||||
|
<%= govuk_link_to "Return to schemes", schemes_path %> |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,16 @@ |
|||||||
|
<% content_for :title, "Download CSV" %> |
||||||
|
|
||||||
|
<% content_for :before_content do %> |
||||||
|
<%= govuk_back_link(href: :back) %> |
||||||
|
<% end %> |
||||||
|
|
||||||
|
<div class="govuk-grid-row"> |
||||||
|
<div class="govuk-grid-column-full"> |
||||||
|
<h1 class="govuk-heading-l">Download CSV</h1> |
||||||
|
|
||||||
|
<p class="govuk-body">We'll send a secure download link to your email address <strong><%= @current_user.email %></strong>.</p> |
||||||
|
<p class="govuk-body"><%= selected_schemes_and_locations_text(download_type, schemes) %></p> |
||||||
|
|
||||||
|
<%= govuk_button_to "Send email", post_path, method: :post, params: { search: search_term, download_type: } %> |
||||||
|
</div> |
||||||
|
</div> |
|
|
|
@ -0,0 +1,94 @@ |
|||||||
|
require "rails_helper" |
||||||
|
|
||||||
|
describe SchemeEmailCsvJob do |
||||||
|
include Helpers |
||||||
|
|
||||||
|
test_url = :test_url |
||||||
|
|
||||||
|
let(:job) { described_class.new } |
||||||
|
let(:user) { FactoryBot.create(:user) } |
||||||
|
let(:storage_service) { instance_double(Storage::S3Service) } |
||||||
|
let(:mailer) { instance_double(CsvDownloadMailer) } |
||||||
|
let(:scheme_csv_service) { instance_double(Csv::SchemeCsvService) } |
||||||
|
let(:search_term) { "meaning" } |
||||||
|
let(:filters) { { "owning_organisation" => organisation.id, "status" => %w[active] } } |
||||||
|
let(:all_orgs) { false } |
||||||
|
let(:organisation) { build(:organisation) } |
||||||
|
let(:download_type) { "combined" } |
||||||
|
let(:schemes) { build_list(:scheme, 5, owning_organisation: organisation) } |
||||||
|
let(:locations) { build_list(:location, 5, scheme: schemes.first) } |
||||||
|
|
||||||
|
before do |
||||||
|
allow(Storage::S3Service).to receive(:new).and_return(storage_service) |
||||||
|
allow(storage_service).to receive(:write_file) |
||||||
|
allow(storage_service).to receive(:get_presigned_url).and_return(test_url) |
||||||
|
|
||||||
|
allow(Csv::SchemeCsvService).to receive(:new).and_return(scheme_csv_service) |
||||||
|
allow(scheme_csv_service).to receive(:prepare_csv).and_return("") |
||||||
|
|
||||||
|
allow(CsvDownloadMailer).to receive(:new).and_return(mailer) |
||||||
|
allow(mailer).to receive(:send_csv_download_mail) |
||||||
|
end |
||||||
|
|
||||||
|
context "when exporting" do |
||||||
|
before do |
||||||
|
allow(FilterManager).to receive(:filter_schemes).and_return(schemes) |
||||||
|
end |
||||||
|
|
||||||
|
context "when download type schemes" do |
||||||
|
let(:download_type) { "schemes" } |
||||||
|
|
||||||
|
it "uses an appropriate filename in S3" do |
||||||
|
expect(storage_service).to receive(:write_file).with(/schemes-.*\.csv/, anything) |
||||||
|
job.perform(user) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "when download type locations" do |
||||||
|
let(:download_type) { "locations" } |
||||||
|
|
||||||
|
it "uses an appropriate filename in S3" do |
||||||
|
expect(storage_service).to receive(:write_file).with(/locations-.*\.csv/, anything) |
||||||
|
job.perform(user) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "when download type combined" do |
||||||
|
let(:download_type) { "combined" } |
||||||
|
|
||||||
|
it "uses an appropriate filename in S3" do |
||||||
|
expect(storage_service).to receive(:write_file).with(/schemes-and-locations.*\.csv/, anything) |
||||||
|
job.perform(user) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it "includes the organisation name in the filename when one is provided" do |
||||||
|
expect(storage_service).to receive(:write_file).with(/schemes-and-locations-#{organisation.name}-.*\.csv/, anything) |
||||||
|
job.perform(user, nil, {}, nil, organisation, "combined") |
||||||
|
end |
||||||
|
|
||||||
|
it "calls the filter manager with the arguments provided" do |
||||||
|
expect(FilterManager).to receive(:filter_schemes).with(a_kind_of(ActiveRecord::Relation), search_term, filters, all_orgs, user) |
||||||
|
job.perform(user, search_term, filters, all_orgs, organisation, "combined") |
||||||
|
end |
||||||
|
|
||||||
|
it "creates a SchemeCsvService with the correct download type" do |
||||||
|
expect(Csv::SchemeCsvService).to receive(:new).with(download_type: "schemes") |
||||||
|
job.perform(user, nil, {}, nil, nil, "schemes") |
||||||
|
expect(Csv::SchemeCsvService).to receive(:new).with(download_type: "locations") |
||||||
|
job.perform(user, nil, {}, nil, nil, "locations") |
||||||
|
expect(Csv::SchemeCsvService).to receive(:new).with(download_type: "combined") |
||||||
|
job.perform(user, nil, {}, nil, nil, "combined") |
||||||
|
end |
||||||
|
|
||||||
|
it "passes the schemes returned by the filter manager to the csv service" do |
||||||
|
expect(scheme_csv_service).to receive(:prepare_csv).with(schemes) |
||||||
|
job.perform(user, nil, {}, nil, nil, "combined") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it "sends an E-mail with the presigned URL and duration" do |
||||||
|
expect(mailer).to receive(:send_csv_download_mail).with(user, test_url, instance_of(Integer)) |
||||||
|
job.perform(user) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,147 @@ |
|||||||
|
require "rails_helper" |
||||||
|
|
||||||
|
RSpec.describe Csv::SchemeCsvService do |
||||||
|
let(:organisation) { create(:organisation) } |
||||||
|
let(:fixed_time) { Time.zone.local(2023, 6, 26) } |
||||||
|
let(:scheme) { create(:scheme, :export, owning_organisation: organisation, service_name: "Test name") } |
||||||
|
let(:location) { create(:location, :export, scheme:) } |
||||||
|
let(:service) { described_class.new(download_type:) } |
||||||
|
let(:download_type) { "combined" } |
||||||
|
let(:csv) { CSV.parse(service.prepare_csv(Scheme.where(id: schemes.map(&:id)))) } |
||||||
|
let(:schemes) { [scheme] } |
||||||
|
let(:headers) { csv.first } |
||||||
|
|
||||||
|
before do |
||||||
|
Timecop.freeze(fixed_time) |
||||||
|
create(:scheme_deactivation_period, scheme:, deactivation_date: scheme.created_at + 1.year, reactivation_date: scheme.created_at + 2.years) |
||||||
|
create(:location_deactivation_period, location:, deactivation_date: location.created_at + 6.months) |
||||||
|
end |
||||||
|
|
||||||
|
after do |
||||||
|
Timecop.return |
||||||
|
end |
||||||
|
|
||||||
|
it "returns a string" do |
||||||
|
result = service.prepare_csv(Scheme.all) |
||||||
|
expect(result).to be_a String |
||||||
|
end |
||||||
|
|
||||||
|
it "returns a csv with headers" do |
||||||
|
expect(csv.first.first).to eq "scheme_code" |
||||||
|
end |
||||||
|
|
||||||
|
it "returns the correctly formatted scheme code" do |
||||||
|
expect(csv.second.first.first).to eq "S" |
||||||
|
end |
||||||
|
|
||||||
|
context "when download type is schemes" do |
||||||
|
let(:download_type) { "schemes" } |
||||||
|
let(:scheme_attributes) { %w[scheme_code scheme_service_name scheme_status scheme_sensitive scheme_type scheme_registered_under_care_act scheme_owning_organisation_name scheme_support_services_provided_by scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at scheme_active_dates] } |
||||||
|
|
||||||
|
it "has the correct headers" do |
||||||
|
expect(headers).to eq(scheme_attributes) |
||||||
|
end |
||||||
|
|
||||||
|
it "exports the CSV with all values correct" do |
||||||
|
expected_content = CSV.read("spec/fixtures/files/schemes_csv_export.csv") |
||||||
|
values_to_delete = %w[scheme_code] |
||||||
|
values_to_delete.each do |attribute| |
||||||
|
index = csv.first.index(attribute) |
||||||
|
csv.second[index] = nil |
||||||
|
end |
||||||
|
expect(csv).to eq expected_content |
||||||
|
end |
||||||
|
|
||||||
|
context "when there are many schemes and locations" do |
||||||
|
let(:schemes) { create_list(:scheme, scheme_count) } |
||||||
|
let(:scheme_count) { 5 } |
||||||
|
let(:locations_per_scheme) { 2 } |
||||||
|
|
||||||
|
before do |
||||||
|
schemes.each do |scheme| |
||||||
|
create_list(:location, locations_per_scheme, scheme:) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it "creates a CSV with the correct number of schemes" do |
||||||
|
expected_row_count_with_headers = scheme_count + 1 |
||||||
|
expect(csv.size).to be expected_row_count_with_headers |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "when download type is locations" do |
||||||
|
let(:download_type) { "locations" } |
||||||
|
let(:location_attributes) { %w[scheme_code location_code location_postcode location_name location_status location_local_authority location_units location_type_of_unit location_mobility_type location_active_dates] } |
||||||
|
|
||||||
|
it "has the correct headers" do |
||||||
|
expect(headers).to eq(location_attributes) |
||||||
|
end |
||||||
|
|
||||||
|
it "exports the CSV with all values correct" do |
||||||
|
expected_content = CSV.read("spec/fixtures/files/locations_csv_export.csv") |
||||||
|
values_to_delete = %w[scheme_code location_code] |
||||||
|
values_to_delete.each do |attribute| |
||||||
|
index = csv.first.index(attribute) |
||||||
|
csv.second[index] = nil |
||||||
|
end |
||||||
|
expect(csv).to eq expected_content |
||||||
|
end |
||||||
|
|
||||||
|
context "when there are many schemes and locations" do |
||||||
|
let(:schemes) { create_list(:scheme, scheme_count) } |
||||||
|
let(:scheme_count) { 5 } |
||||||
|
let(:locations_per_scheme) { 2 } |
||||||
|
|
||||||
|
before do |
||||||
|
schemes.each do |scheme| |
||||||
|
create_list(:location, locations_per_scheme, scheme:) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it "creates a CSV with the correct number of locations" do |
||||||
|
expected_row_count_with_headers = locations_per_scheme * scheme_count + 1 |
||||||
|
expect(csv.size).to be expected_row_count_with_headers |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
context "when download type is combined" do |
||||||
|
let(:combined_attributes) { %w[scheme_code scheme_service_name scheme_status scheme_sensitive scheme_type scheme_registered_under_care_act scheme_owning_organisation_name scheme_support_services_provided_by scheme_primary_client_group scheme_has_other_client_group scheme_secondary_client_group scheme_support_type scheme_intended_stay scheme_created_at scheme_active_dates location_code location_postcode location_name location_status location_local_authority location_units location_type_of_unit location_mobility_type location_active_dates] } |
||||||
|
|
||||||
|
before do |
||||||
|
scheme |
||||||
|
end |
||||||
|
|
||||||
|
it "has the correct headers" do |
||||||
|
expect(headers).to eq(combined_attributes) |
||||||
|
end |
||||||
|
|
||||||
|
it "exports the CSV with all values correct" do |
||||||
|
expected_content = CSV.read("spec/fixtures/files/schemes_and_locations_csv_export.csv") |
||||||
|
values_to_delete = %w[scheme_code location_code] |
||||||
|
values_to_delete.each do |attribute| |
||||||
|
index = csv.first.index(attribute) |
||||||
|
csv.second[index] = nil |
||||||
|
end |
||||||
|
expect(csv).to eq expected_content |
||||||
|
end |
||||||
|
|
||||||
|
context "when there are many schemes and locations" do |
||||||
|
let(:schemes) { create_list(:scheme, scheme_count) } |
||||||
|
let(:scheme_count) { 5 } |
||||||
|
let(:locations_per_scheme) { 2 } |
||||||
|
|
||||||
|
before do |
||||||
|
schemes.each do |scheme| |
||||||
|
create_list(:location, locations_per_scheme, scheme:) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
it "creates a CSV with the correct number of locations" do |
||||||
|
expected_row_count_with_headers = locations_per_scheme * scheme_count + 1 |
||||||
|
expect(csv.size).to be expected_row_count_with_headers |
||||||
|
end |
||||||
|
end |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue