diff --git a/app/components/search_result_caption_component.html.erb b/app/components/search_result_caption_component.html.erb index d170922d7..8f3d7e5c3 100644 --- a/app/components/search_result_caption_component.html.erb +++ b/app/components/search_result_caption_component.html.erb @@ -1,10 +1,10 @@ <% if searched.present? && filters_count&.positive? %> - <%= count %> <%= item_label.pluralize(count) %> matching search and filters
+ <%= count %> <%= item_label.pluralize(count) %> matching search and filters <% elsif searched.present? %> - <%= count %> <%= item_label.pluralize(count) %> matching search
+ <%= count %> <%= item_label.pluralize(count) %> matching search <% elsif filters_count&.positive? %> - <%= count %> <%= item_label.pluralize(count) %> matching filters
+ <%= count %> <%= item_label.pluralize(count) %> matching filters <% else %> <%= count %> matching <%= item %> <% end %> diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 41213ba9c..3a5cd27c6 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -29,6 +29,18 @@ class OrganisationsController < ApplicationController @filter_type = "schemes" end + def download_schemes_csv + organisation_schemes = Scheme.where(owning_organisation_id: @organisation.id) + unpaginated_filtered_schemes = filter_manager.filtered_schemes(organisation_schemes, search_term, session_filters) + + render "schemes/download_csv", locals: { search_term:, post_path: email_csv_schemes_path, download_type: params[:download_type], schemes: unpaginated_filtered_schemes } + end + + def email_schemes_csv + SchemesEmailCsvJob.perform_later(current_user, search_term, session_filters, false, @organisation) + redirect_to schemes_csv_confirmation_organisation_path + end + def show redirect_to details_organisation_path(@organisation) end diff --git a/app/controllers/schemes_controller.rb b/app/controllers/schemes_controller.rb index 427931b3e..4e21ce648 100644 --- a/app/controllers/schemes_controller.rb +++ b/app/controllers/schemes_controller.rb @@ -3,11 +3,11 @@ class SchemesController < ApplicationController include Modules::SearchFilter before_action :authenticate_user! - before_action :find_resource, except: %i[index create new changes] + before_action :find_resource, except: %i[index create new changes email_csv download_csv csv_confirmation] before_action :redirect_if_scheme_confirmed, only: %i[primary_client_group confirm_secondary_client_group secondary_client_group support details] - before_action :authorize_user - before_action :session_filters, if: :current_user, only: %i[index] - before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index] + before_action :authorize_user, except: %i[email_csv download_csv csv_confirmation] + before_action :session_filters, if: :current_user, only: %i[index email_csv download_csv] + before_action -> { filter_manager.serialize_filters_to_session }, if: :current_user, only: %i[index email_csv download_csv] rescue_from ActiveRecord::RecordNotFound, with: :render_not_found @@ -205,6 +205,20 @@ class SchemesController < ApplicationController render "schemes/changes" end + def download_csv + unpaginated_filtered_schemes = filter_manager.filtered_schemes(current_user.schemes, search_term, session_filters) + + render "download_csv", locals: { search_term:, post_path: email_csv_schemes_path, download_type: params[:download_type], schemes: unpaginated_filtered_schemes } + end + + def email_csv + all_orgs = params["organisation_select"] == "all" + SchemesEmailCsvJob.perform_later(current_user, search_term, session_filters, all_orgs, nil) + redirect_to csv_confirmation_schemes_path + end + + def csv_confirmation; end + private def authorize_user diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index a1ec640eb..8dee3c9be 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -54,6 +54,20 @@ module SchemesHelper end end + def selected_schemes_and_locations_text(download_type, schemes) + scheme_count = schemes.count + case download_type + when "schemes" + "You've selected #{pluralize(scheme_count, 'scheme')}." + when "locations" + location_count = schemes.map(&:locations).flatten.count + "You've selected #{pluralize(location_count, 'location')} from #{pluralize(scheme_count, 'scheme')}." + when "combined" + location_count = schemes.map(&:locations).flatten.count + "You've selected #{pluralize(scheme_count, 'scheme')} with #{pluralize(location_count, 'location')}. The CSV will have one location per row with scheme details listed for each location." + end + end + private ActivePeriod = Struct.new(:from, :to) diff --git a/app/jobs/email_scheme_csv_job.rb b/app/jobs/email_scheme_csv_job.rb new file mode 100644 index 000000000..2165471e8 --- /dev/null +++ b/app/jobs/email_scheme_csv_job.rb @@ -0,0 +1,33 @@ +class EmailSchemeCsvJob < 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.visible + filtered_schemes = FilterManager.filter_schemes(unfiltered_schemes, search_term, filters, all_orgs, user) + + case download_type + when "schemes" + csv_string = Csv::SchemeCsvService.new(user:).prepare_csv(filtered_schemes) + filename = "#{['schemes', organisation&.name, Time.zone.now].compact.join('-')}.csv" + when "locations" + filtered_locations = filtered_schemes.map(&:locations).flatten + csv_string = Csv::LocationCsvService.new(user:).prepare_csv(filtered_locations) + filename = "#{['locations', organisation&.name, Time.zone.now].compact.join('-')}.csv" + when "combined" + filtered_locations = filtered_schemes.map(&:locations).flatten + csv_string = Csv::SchemeAndLocationCsvService.new(user:).prepare_csv(filtered_locations) + 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 diff --git a/app/models/user.rb b/app/models/user.rb index 52e2c850d..38fa71692 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,6 +99,14 @@ class User < ApplicationRecord LettingsLog.filter_by_managing_organisation(organisation.absorbed_organisations + [organisation]) end + def schemes + if support? + Scheme.all + else + Scheme.filter_by_owning_organisation(organisation.absorbed_organisations + [organisation]) + end + end + def is_key_contact? is_key_contact end diff --git a/app/views/organisations/schemes.html.erb b/app/views/organisations/schemes.html.erb index 37bc7ba44..44f089f55 100644 --- a/app/views/organisations/schemes.html.erb +++ b/app/views/organisations/schemes.html.erb @@ -27,7 +27,11 @@
- <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %> + <% if current_user.support? %> + <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, schemes_csv_download_url: schemes_csv_download_organisation_path(@organisation, search: @searched, download_type: "schemes"), locations_csv_download_url: schemes_csv_download_organisation_path(@organisation, search: @searched, download_type: "locations"), combined_csv_download_url: schemes_csv_download_organisation_path(@organisation, search: @searched, download_type: "combined") } %> + <% else %> + <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, schemes_csv_download_url: csv_download_schemes_path(search: @searched, download_type: "schemes"), locations_csv_download_url: csv_download_schemes_path(search: @searched, download_type: "locations"), combined_csv_download_url: csv_download_schemes_path(search: @searched, download_type: "combined") } %> + <% end %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %> diff --git a/app/views/schemes/_scheme_list.html.erb b/app/views/schemes/_scheme_list.html.erb index b898e6018..2ade6fd1e 100644 --- a/app/views/schemes/_scheme_list.html.erb +++ b/app/views/schemes/_scheme_list.html.erb @@ -1,8 +1,13 @@
<%= govuk_table do |table| %> <%= table.caption(classes: %w[govuk-!-font-size-19 govuk-!-font-weight-regular]) do |caption| %> - <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "schemes", filters_count: applied_filters_count(@filter_type))) %> + <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "schemes", filters_count: applied_filters_count(@filter_type))) %> + <% if @schemes&.any? %> + <%= govuk_link_to "Download schemes (CSV)", schemes_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> + <%= govuk_link_to "Download locations (CSV)", locations_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> + <%= govuk_link_to "Download schemes and locations (CSV)", combined_csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> <% end %> + <% end %> <%= table.head do |head| %> <%= head.row do |row| %> <% row.cell(header: true, text: "Scheme", html_attributes: { scope: "col", class: "govuk-!-width-one-quarter" }) %> diff --git a/app/views/schemes/download_csv.html.erb b/app/views/schemes/download_csv.html.erb new file mode 100644 index 000000000..b2cfe55e0 --- /dev/null +++ b/app/views/schemes/download_csv.html.erb @@ -0,0 +1,16 @@ +<% content_for :title, "Download CSV" %> + +<% content_for :before_content do %> + <%= govuk_back_link(href: :back) %> +<% end %> + +
+
+

Download CSV

+ +

We'll send a secure download link to your email address <%= @current_user.email %>.

+

<%= selected_schemes_and_locations_text(download_type, schemes) %>

+ + <%= govuk_button_to "Send email", post_path, method: :post, params: { search: search_term } %> +
+
diff --git a/app/views/schemes/index.html.erb b/app/views/schemes/index.html.erb index 4f3218e7a..036f9b719 100644 --- a/app/views/schemes/index.html.erb +++ b/app/views/schemes/index.html.erb @@ -15,8 +15,8 @@
- <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count } %> - + <%= render partial: "schemes/scheme_list", locals: { schemes: @schemes, title:, pagy: @pagy, searched: @searched, item_label:, total_count: @total_count, schemes_csv_download_url: csv_download_schemes_path(search: @searched, download_type: "schemes"), locations_csv_download_url: csv_download_schemes_path(search: @searched, download_type: "locations"), combined_csv_download_url: csv_download_schemes_path(search: @searched, download_type: "combined") } %> + <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %> diff --git a/config/routes.rb b/config/routes.rb index 3c58e5a29..af0b95d07 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -79,6 +79,12 @@ Rails.application.routes.draw do patch "deactivate", to: "schemes#deactivate" patch "reactivate", to: "schemes#reactivate" + collection do + get "csv-download", to: "schemes#download_csv" + post "email-csv", to: "schemes#email_csv" + get "csv-confirmation", to: "schemes#csv_confirmation" + end + resources :locations do post "locations", to: "locations#create" get "new-deactivation", to: "locations#new_deactivation" @@ -148,6 +154,9 @@ Rails.application.routes.draw do post "sales-logs/email-csv", to: "organisations#email_sales_csv" get "sales-logs/csv-confirmation", to: "sales_logs#csv_confirmation" get "schemes", to: "organisations#schemes" + get "schemes/csv-download", to: "organisations#download_schemes_csv" + post "schemes/email-csv", to: "organisations#email_schemes_csv" + get "schemes/csv-confirmation", to: "schemes#csv_confirmation" get "stock-owners", to: "organisation_relationships#stock_owners" get "stock-owners/add", to: "organisation_relationships#add_stock_owner" get "stock-owners/remove", to: "organisation_relationships#remove_stock_owner" diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 4bd6e0eb6..ce6747f41 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -59,6 +59,62 @@ RSpec.describe OrganisationsController, type: :request do expect(page).to have_field("search", type: "search") end + describe "scheme and location csv downloads" do + let!(:specific_organisation) { create(:organisation) } + let!(:specific_org_schemes) { create_list(:scheme, 5, owning_organisation: specific_organisation) } + let!(:specific_org_scheme) { create(:scheme, owning_organisation: specific_organisation) } + let!(:specific_org_locations) { create_list(:location, 3, scheme: specific_org_scheme) } + + it "shows scheme and location download links" do + expect(page).to have_link("Download schemes (CSV)", href: csv_download_schemes_path(download_type: "schemes")) + expect(page).to have_link("Download locations (CSV)", href: csv_download_schemes_path(download_type: "locations")) + expect(page).to have_link("Download schemes and locations (CSV)", href: csv_download_schemes_path(download_type: "combined")) + end + + context "when there are no schemes for this organisation" do + before do + specific_organisation.owned_schemes.destroy_all + get "/organisations/#{specific_organisation.id}/schemes", headers:, params: {} + end + + it "does not display CSV download links" do + expect(page).not_to have_link("Download schemes (CSV)") + expect(page).not_to have_link("Download locations (CSV)") + expect(page).not_to have_link("Download schemes and locations (CSV)") + end + end + + context "when downloading scheme data" do + before do + get csv_download_schemes_path(download_type: "schemes") + end + + it "redirects to the correct download page" do + expect(page).to have_content("You've selected 6 schemes.") + end + end + + context "when downloading location data" do + before do + get csv_download_schemes_path(download_type: "locations") + end + + it "redirects to the correct download page" do + expect(page).to have_content("You've selected 3 locations from 6 schemes.") + end + end + + context "when downloading scheme and location data" do + before do + get csv_download_schemes_path(download_type: "combined") + end + + it "redirects to the correct download page" do + expect(page).to have_content("You've selected 6 schemes with 3 locations.") + end + end + end + it "has hidden accessibility field with description" do expected_field = "

Supported housing schemes

" expect(CGI.unescape_html(response.body)).to include(expected_field) @@ -116,6 +172,60 @@ RSpec.describe OrganisationsController, type: :request do expect(page).to have_field("search", type: "search") end + describe "scheme and location csv downloads" do + let!(:same_org_schemes) { create_list(:scheme, 5, owning_organisation: user.organisation) } + let!(:same_org_locations) { create_list(:location, 3, scheme: same_org_scheme) } + + it "shows scheme and location download links" do + expect(page).to have_link("Download schemes (CSV)", href: csv_download_schemes_path(download_type: "schemes")) + expect(page).to have_link("Download locations (CSV)", href: csv_download_schemes_path(download_type: "locations")) + expect(page).to have_link("Download schemes and locations (CSV)", href: csv_download_schemes_path(download_type: "combined")) + end + + context "when there are no schemes for this organisation" do + before do + user.organisation.owned_schemes.destroy_all + get "/organisations/#{organisation.id}/schemes", headers:, params: {} + end + + it "does not display CSV download links" do + expect(page).not_to have_link("Download schemes (CSV)") + expect(page).not_to have_link("Download locations (CSV)") + expect(page).not_to have_link("Download schemes and locations (CSV)") + end + end + + context "when downloading scheme data" do + before do + get csv_download_schemes_path(download_type: "schemes") + end + + it "redirects to the correct download page" do + expect(page).to have_content("You've selected 6 schemes.") + end + end + + context "when downloading location data" do + before do + get csv_download_schemes_path(download_type: "locations") + end + + it "redirects to the correct download page" do + expect(page).to have_content("You've selected 3 locations from 6 schemes.") + end + end + + context "when downloading scheme and location data" do + before do + get csv_download_schemes_path(download_type: "combined") + end + + it "redirects to the correct download page" do + expect(page).to have_content("You've selected 6 schemes with 3 locations.") + end + end + end + it "shows only schemes belonging to the same organisation" do expect(page).to have_content(same_org_scheme.id_to_display) schemes.each do |scheme|