From 9b19b1eedc38338802b42b28263fdfd5fb116d60 Mon Sep 17 00:00:00 2001 From: natdeanlewissoftwire <94526761+natdeanlewissoftwire@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:42:05 +0000 Subject: [PATCH] CLDC-3014 Add schemes and locations csv download functionality (#2083) * 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 spacing --- .../search_result_caption_component.html.erb | 6 +- app/controllers/organisations_controller.rb | 22 ++- app/controllers/schemes_controller.rb | 24 ++- app/frontend/styles/_search.scss | 4 + app/helpers/navigation_items_helper.rb | 2 +- app/helpers/schemes_helper.rb | 22 +++ app/jobs/scheme_email_csv_job.rb | 29 ++++ app/models/user.rb | 8 + app/services/csv/scheme_csv_service.rb | 108 +++++++++++++ app/services/filter_manager.rb | 2 +- app/views/logs/_log_list.html.erb | 6 +- app/views/logs/download_csv.html.erb | 2 +- app/views/organisations/schemes.html.erb | 6 +- app/views/schemes/_scheme_list.html.erb | 9 +- app/views/schemes/csv_confirmation.html.erb | 15 ++ app/views/schemes/download_csv.html.erb | 16 ++ app/views/schemes/index.html.erb | 2 +- app/views/users/_user_list.html.erb | 2 +- config/routes.rb | 9 ++ db/seeds.rb | 9 +- .../search_result_caption_component_spec.rb | 12 +- spec/features/organisation_spec.rb | 4 +- spec/features/schemes_spec.rb | 4 +- spec/fixtures/files/locations_csv_export.csv | 2 + .../schemes_and_locations_csv_export.csv | 2 + spec/fixtures/files/schemes_csv_export.csv | 2 + spec/jobs/scheme_email_csv_job_spec.rb | 94 +++++++++++ .../requests/lettings_logs_controller_spec.rb | 4 +- ...anisation_relationships_controller_spec.rb | 12 +- .../requests/organisations_controller_spec.rb | 126 ++++++++++++++- spec/requests/sales_logs_controller_spec.rb | 4 +- spec/requests/schemes_controller_spec.rb | 67 +++++++- spec/requests/users_controller_spec.rb | 2 +- spec/services/csv/scheme_csv_service_spec.rb | 147 ++++++++++++++++++ spec/services/filter_manager_spec.rb | 17 ++ 35 files changed, 748 insertions(+), 54 deletions(-) create mode 100644 app/jobs/scheme_email_csv_job.rb create mode 100644 app/services/csv/scheme_csv_service.rb create mode 100644 app/views/schemes/csv_confirmation.html.erb create mode 100644 app/views/schemes/download_csv.html.erb create mode 100644 spec/fixtures/files/locations_csv_export.csv create mode 100644 spec/fixtures/files/schemes_and_locations_csv_export.csv create mode 100644 spec/fixtures/files/schemes_csv_export.csv create mode 100644 spec/jobs/scheme_email_csv_job_spec.rb create mode 100644 spec/services/csv/scheme_csv_service_spec.rb diff --git a/app/components/search_result_caption_component.html.erb b/app/components/search_result_caption_component.html.erb index d170922d7..b8a9382b7 100644 --- a/app/components/search_result_caption_component.html.erb +++ b/app/components/search_result_caption_component.html.erb @@ -1,4 +1,4 @@ - + <% if searched.present? && filters_count&.positive? %> <%= count %> <%= item_label.pluralize(count) %> matching search and filters
<% elsif searched.present? %> @@ -6,6 +6,8 @@ <% elsif filters_count&.positive? %> <%= count %> <%= item_label.pluralize(count) %> matching filters
<% else %> - <%= count %> matching <%= item %> + + <%= count %> total <%= item %> + <% end %>
diff --git a/app/controllers/organisations_controller.rb b/app/controllers/organisations_controller.rb index 2b902d129..3bf4cf5a9 100644 --- a/app/controllers/organisations_controller.rb +++ b/app/controllers/organisations_controller.rb @@ -7,9 +7,9 @@ class OrganisationsController < ApplicationController before_action :find_resource, except: %i[index new create] before_action :authenticate_scope!, except: [:index] before_action :session_filters, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_lettings_csv download_lettings_csv email_sales_csv download_sales_csv] - before_action :session_filters, only: %i[users schemes] + before_action :session_filters, only: %i[users schemes email_schemes_csv download_schemes_csv] before_action -> { filter_manager.serialize_filters_to_session }, if: -> { current_user.support? || current_user.organisation.has_managing_agents? }, only: %i[lettings_logs sales_logs email_lettings_csv download_lettings_csv email_sales_csv download_sales_csv] - before_action -> { filter_manager.serialize_filters_to_session }, only: %i[users schemes] + before_action -> { filter_manager.serialize_filters_to_session }, only: %i[users schemes email_schemes_csv download_schemes_csv] def index redirect_to organisation_path(current_user.organisation) unless current_user.support? @@ -21,14 +21,26 @@ class OrganisationsController < ApplicationController end def schemes - all_schemes = Scheme.where(owning_organisation: [@organisation] + @organisation.parent_organisations) + organisation_schemes = Scheme.where(owning_organisation: [@organisation] + @organisation.parent_organisations) - @pagy, @schemes = pagy(filter_manager.filtered_schemes(all_schemes, search_term, session_filters).order_by_service_name) + @pagy, @schemes = pagy(filter_manager.filtered_schemes(organisation_schemes, search_term, session_filters)) @searched = search_term.presence - @total_count = all_schemes.size + @total_count = organisation_schemes.size @filter_type = "schemes" end + def download_schemes_csv + organisation_schemes = Scheme.where(owning_organisation: [@organisation] + @organisation.parent_organisations) + 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 + SchemeEmailCsvJob.perform_later(current_user, search_term, session_filters, false, @organisation, params[:download_type]) + 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..682e1bc02 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 @@ -15,7 +15,7 @@ class SchemesController < ApplicationController redirect_to schemes_organisation_path(current_user.organisation) unless current_user.support? all_schemes = Scheme.all - @pagy, @schemes = pagy(filter_manager.filtered_schemes(all_schemes, search_term, session_filters).order_by_service_name) + @pagy, @schemes = pagy(filter_manager.filtered_schemes(all_schemes, search_term, session_filters)) @searched = search_term.presence @total_count = all_schemes.size @filter_type = "schemes" @@ -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" + SchemeEmailCsvJob.perform_later(current_user, search_term, session_filters, all_orgs, nil, params[:download_type]) + redirect_to csv_confirmation_schemes_path + end + + def csv_confirmation; end + private def authorize_user diff --git a/app/frontend/styles/_search.scss b/app/frontend/styles/_search.scss index 91399479a..e2b9765b7 100644 --- a/app/frontend/styles/_search.scss +++ b/app/frontend/styles/_search.scss @@ -22,3 +22,7 @@ margin-bottom: 2px; width: auto; } + +.app-search__caption { + line-height: govuk-spacing(7); +} diff --git a/app/helpers/navigation_items_helper.rb b/app/helpers/navigation_items_helper.rb index 6cacaaa18..56954361d 100644 --- a/app/helpers/navigation_items_helper.rb +++ b/app/helpers/navigation_items_helper.rb @@ -57,7 +57,7 @@ private end def supported_housing_schemes_current?(path) - path == schemes_path || path.include?("/schemes/") + path.starts_with?(schemes_path) end def non_support_supported_housing_schemes_current?(path) diff --git a/app/helpers/schemes_helper.rb b/app/helpers/schemes_helper.rb index a1ec640eb..98bd2d62f 100644 --- a/app/helpers/schemes_helper.rb +++ b/app/helpers/schemes_helper.rb @@ -54,6 +54,28 @@ 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 + + def primary_schemes_csv_download_url(search, download_type) + csv_download_schemes_path(search:, download_type:) + end + + def secondary_schemes_csv_download_url(organisation, search, download_type) + schemes_csv_download_organisation_path(organisation, search:, download_type:) + end + private ActivePeriod = Struct.new(:from, :to) diff --git a/app/jobs/scheme_email_csv_job.rb b/app/jobs/scheme_email_csv_job.rb new file mode 100644 index 000000000..1dad752c5 --- /dev/null +++ b/app/jobs/scheme_email_csv_job.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 4e80f7382..bd04ba2b2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -107,6 +107,14 @@ class User < ApplicationRecord SalesLog.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/services/csv/scheme_csv_service.rb b/app/services/csv/scheme_csv_service.rb new file mode 100644 index 000000000..a45266f58 --- /dev/null +++ b/app/services/csv/scheme_csv_service.rb @@ -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 diff --git a/app/services/filter_manager.rb b/app/services/filter_manager.rb index c8665752d..1a824b2e2 100644 --- a/app/services/filter_manager.rb +++ b/app/services/filter_manager.rb @@ -59,7 +59,7 @@ class FilterManager schemes = schemes.public_send("filter_by_#{category}", values, user) end - schemes + schemes.order_by_service_name end def self.filter_locations(locations, search_term, filters, user) diff --git a/app/views/logs/_log_list.html.erb b/app/views/logs/_log_list.html.erb index 74dde785d..b5290c117 100644 --- a/app/views/logs/_log_list.html.erb +++ b/app/views/logs/_log_list.html.erb @@ -1,11 +1,11 @@

-
+
<%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "logs", filters_count: applied_filters_count(@filter_type))) %> <% if logs&.any? %> - <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %> + <%= govuk_link_to "Download (CSV)", csv_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> <% if @current_user.support? %> - <%= govuk_link_to "Download (CSV, codes only)", csv_codes_only_download_url, type: "text/csv", class: "govuk-!-margin-right-4" %> + <%= govuk_link_to "Download (CSV, codes only)", csv_codes_only_download_url, type: "text/csv", class: "govuk-!-margin-right-4", style: "white-space: nowrap" %> <% end %> <% end %>
diff --git a/app/views/logs/download_csv.html.erb b/app/views/logs/download_csv.html.erb index 6da8b46fd..edcf25543 100644 --- a/app/views/logs/download_csv.html.erb +++ b/app/views/logs/download_csv.html.erb @@ -5,7 +5,7 @@ <% end %>
-
+

Download CSV

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

diff --git a/app/views/organisations/schemes.html.erb b/app/views/organisations/schemes.html.erb index 37bc7ba44..58b16243a 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: secondary_schemes_csv_download_url(@organisation, @searched, "schemes"), locations_csv_download_url: secondary_schemes_csv_download_url(@organisation, @searched, "locations"), combined_csv_download_url: secondary_schemes_csv_download_url(@organisation, @searched, "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: primary_schemes_csv_download_url(@searched, "schemes"), locations_csv_download_url: primary_schemes_csv_download_url(@searched, "locations"), combined_csv_download_url: primary_schemes_csv_download_url(@searched, "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..8686f3f12 100644 --- a/app/views/schemes/_scheme_list.html.erb +++ b/app/views/schemes/_scheme_list.html.erb @@ -1,8 +1,15 @@
<%= 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/csv_confirmation.html.erb b/app/views/schemes/csv_confirmation.html.erb new file mode 100644 index 000000000..0746347cf --- /dev/null +++ b/app/views/schemes/csv_confirmation.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, "We’re sending you an email" %> +
+
+ <%= govuk_panel(title_text: "We’re sending you an email") %> + +

It should arrive in a few minutes, but it could take longer.

+ +

What happens next

+

Open your email inbox and click the link to download your CSV file.

+ +

+ <%= govuk_link_to "Return to schemes", schemes_path %> +

+
+
diff --git a/app/views/schemes/download_csv.html.erb b/app/views/schemes/download_csv.html.erb new file mode 100644 index 000000000..af876bc90 --- /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, download_type: } %> +
+
diff --git a/app/views/schemes/index.html.erb b/app/views/schemes/index.html.erb index 4f3218e7a..d0ae44072 100644 --- a/app/views/schemes/index.html.erb +++ b/app/views/schemes/index.html.erb @@ -15,7 +15,7 @@
- <%= 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: primary_schemes_csv_download_url(@searched, "schemes"), locations_csv_download_url: primary_schemes_csv_download_url(@searched, "locations"), combined_csv_download_url: primary_schemes_csv_download_url(@searched, "combined") } %> <%== render partial: "pagy/nav", locals: { pagy: @pagy, item_name: "schemes" } %>
diff --git a/app/views/users/_user_list.html.erb b/app/views/users/_user_list.html.erb index 389053294..9ff4ebc4b 100644 --- a/app/views/users/_user_list.html.erb +++ b/app/views/users/_user_list.html.erb @@ -4,7 +4,7 @@ <%= render(SearchResultCaptionComponent.new(searched:, count: pagy.count, item_label:, total_count:, item: "users", filters_count: applied_filters_count(@filter_type))) %> <% if current_user.support? %> <% query = searched.present? ? "?search=#{searched}" : nil %> - <%= govuk_link_to "Download (CSV)", "#{request.path}.csv#{query}", type: "text/csv" %> + <%= govuk_link_to "Download (CSV)", "#{request.path}.csv#{query}", type: "text/csv", style: "white-space: nowrap" %> <% end %> <% end %> <%= table.head do |head| %> 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/db/seeds.rb b/db/seeds.rb index 805f0c53a..76da9ad35 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -301,7 +301,8 @@ unless Rails.env.test? Location.create!( scheme: scheme1, - location_code: "S254-CU193AA", + location_code: "E09000033", + location_admin_district: "Westminster", postcode: "CU193AA", name: "Rectory Road", type_of_unit: 4, @@ -311,7 +312,8 @@ unless Rails.env.test? Location.create!( scheme: scheme1, - location_code: "S254-DM250DC", + location_code: "E09000033", + location_admin_district: "Westminster", postcode: "DM250DC", name: "Smithy Lane", type_of_unit: 1, @@ -321,7 +323,8 @@ unless Rails.env.test? Location.create!( scheme: scheme2, - location_code: "S254-YX130WP", + location_code: "E09000033", + location_admin_district: "Westminster", postcode: "YX130WP", name: "Smithy Lane", type_of_unit: 2, diff --git a/spec/components/search_result_caption_component_spec.rb b/spec/components/search_result_caption_component_spec.rb index 3e6baaada..25cbc1bdd 100644 --- a/spec/components/search_result_caption_component_spec.rb +++ b/spec/components/search_result_caption_component_spec.rb @@ -11,7 +11,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do context "when search and filter results are found" do it "renders table caption including the search results and total" do - expect(result.to_html).to eq("\n 2 users matching search and filters
\n
\n") + expect(result.to_html).to eq("\n 2 users matching search and filters
\n
\n") end end @@ -19,7 +19,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do let(:filters_count) { nil } it "renders table caption including the search results and total" do - expect(result.to_html).to eq("\n 2 users matching search
\n
\n") + expect(result.to_html).to eq("\n 2 users matching search
\n
\n") end end @@ -27,7 +27,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do let(:searched) { nil } it "renders table caption including the search results and total" do - expect(result.to_html).to eq("\n 2 users matching filters
\n
\n") + expect(result.to_html).to eq("\n 2 users matching filters
\n
\n") end end @@ -36,7 +36,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do let(:filters_count) { nil } it "renders table caption with total count only" do - expect(result.to_html).to eq("\n #{count} matching #{item}\n\n") + expect(result.to_html).to eq("\n \n 2 total schemes\n \n\n") end end @@ -44,7 +44,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do let(:count) { 0 } it "renders table caption with total count only" do - expect(result.to_html).to eq("\n 0 users matching search and filters
\n
\n") + expect(result.to_html).to eq("\n 0 users matching search and filters
\n
\n") end end @@ -52,7 +52,7 @@ RSpec.describe SearchResultCaptionComponent, type: :component do let(:count) { 1 } it "renders table caption with total count only" do - expect(result.to_html).to eq("\n 1 user matching search and filters
\n
\n") + expect(result.to_html).to eq("\n 1 user matching search and filters
\n
\n") end end end diff --git a/spec/features/organisation_spec.rb b/spec/features/organisation_spec.rb index 69a5d8b5c..9cef5edb5 100644 --- a/spec/features/organisation_spec.rb +++ b/spec/features/organisation_spec.rb @@ -189,7 +189,7 @@ RSpec.describe "User Features" do end it "has correct page details" do - expect(page).to have_content("#{number_of_lettings_logs} matching logs") + expect(page).to have_content("#{number_of_lettings_logs} total logs") organisation.lettings_logs.map(&:id).each do |lettings_log_id| expect(page).to have_link lettings_log_id.to_s, href: "/lettings-logs/#{lettings_log_id}" end @@ -237,7 +237,7 @@ RSpec.describe "User Features" do end it "can filter sales logs" do - expect(page).to have_content("#{number_of_sales_logs} matching logs") + expect(page).to have_content("#{number_of_sales_logs} total logs") organisation.sales_logs.map(&:id).each do |sales_log_id| expect(page).to have_link sales_log_id.to_s, href: "/sales-logs/#{sales_log_id}" end diff --git a/spec/features/schemes_spec.rb b/spec/features/schemes_spec.rb index 88c3099a0..491a19bbc 100644 --- a/spec/features/schemes_spec.rb +++ b/spec/features/schemes_spec.rb @@ -573,7 +573,7 @@ RSpec.describe "Schemes scheme Features" do it "displays information about a single location" do expect(page).to have_content "Locations" - expect(page).to have_content "#{scheme.locations.count} matching location" + expect(page).to have_content "#{scheme.locations.count} total location" end it "displays information about the first created location" do @@ -586,7 +586,7 @@ RSpec.describe "Schemes scheme Features" do fill_in_and_save_second_location click_button "Save and return to locations" expect(page).to have_content "Locations" - expect(page).to have_content "#{scheme.locations.count} matching location" + expect(page).to have_content "#{scheme.locations.count} total location" end it "displays information about newly created location" do diff --git a/spec/fixtures/files/locations_csv_export.csv b/spec/fixtures/files/locations_csv_export.csv new file mode 100644 index 000000000..51844b6c5 --- /dev/null +++ b/spec/fixtures/files/locations_csv_export.csv @@ -0,0 +1,2 @@ +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 +,,SW1A 2AA,Downing Street,deactivating_soon,Westminster,20,Self-contained house,Fitted with equipment and adaptations,"Active from 1 April 2022 to 25 December 2023, Deactivated on 26 December 2023" diff --git a/spec/fixtures/files/schemes_and_locations_csv_export.csv b/spec/fixtures/files/schemes_and_locations_csv_export.csv new file mode 100644 index 000000000..9db484412 --- /dev/null +++ b/spec/fixtures/files/schemes_and_locations_csv_export.csv @@ -0,0 +1,2 @@ +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 +,Test name,active,Yes,Housing for older people,No,DLUHC,The same organisation that owns the housing stock,People with alcohol problems,Yes,Older people with support needs,High level,Medium stay,2021-04-01T00:00:00+01:00,"Active from 1 April 2020 to 31 March 2022, Deactivated on 1 April 2022, Active from 1 April 2023",,SW1A 2AA,Downing Street,deactivating_soon,Westminster,20,Self-contained house,Fitted with equipment and adaptations,"Active from 1 April 2022 to 25 December 2023, Deactivated on 26 December 2023" diff --git a/spec/fixtures/files/schemes_csv_export.csv b/spec/fixtures/files/schemes_csv_export.csv new file mode 100644 index 000000000..9fd9928c9 --- /dev/null +++ b/spec/fixtures/files/schemes_csv_export.csv @@ -0,0 +1,2 @@ +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 +,Test name,active,Yes,Housing for older people,No,DLUHC,The same organisation that owns the housing stock,People with alcohol problems,Yes,Older people with support needs,High level,Medium stay,2021-04-01T00:00:00+01:00,"Active from 1 April 2020 to 31 March 2022, Deactivated on 1 April 2022, Active from 1 April 2023" diff --git a/spec/jobs/scheme_email_csv_job_spec.rb b/spec/jobs/scheme_email_csv_job_spec.rb new file mode 100644 index 000000000..d23177ab3 --- /dev/null +++ b/spec/jobs/scheme_email_csv_job_spec.rb @@ -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 diff --git a/spec/requests/lettings_logs_controller_spec.rb b/spec/requests/lettings_logs_controller_spec.rb index 646855f2e..97db942b1 100644 --- a/spec/requests/lettings_logs_controller_spec.rb +++ b/spec/requests/lettings_logs_controller_spec.rb @@ -788,7 +788,7 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows the total log count" do - expect(CGI.unescape_html(response.body)).to match("1 matching logs") + expect(CGI.unescape_html(response.body)).to match("1 total logs") end it "does not show the pagination links" do @@ -882,7 +882,7 @@ RSpec.describe LettingsLogsController, type: :request do end it "shows the total log count" do - expect(CGI.unescape_html(response.body)).to match("26 matching logs") + expect(CGI.unescape_html(response.body)).to match("26 total logs") end it "has pagination links" do diff --git a/spec/requests/organisation_relationships_controller_spec.rb b/spec/requests/organisation_relationships_controller_spec.rb index dcf99efce..a93e77d76 100644 --- a/spec/requests/organisation_relationships_controller_spec.rb +++ b/spec/requests/organisation_relationships_controller_spec.rb @@ -47,7 +47,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 matching stock owners") + expect(page).to have_content("1 total stock owners") end context "when adding a stock owner" do @@ -113,7 +113,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 matching agents") + expect(page).to have_content("1 total agents") end end @@ -285,7 +285,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 matching stock owners") + expect(page).to have_content("1 total stock owners") end end @@ -421,7 +421,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 matching agents") + expect(page).to have_content("1 total agents") end end @@ -587,7 +587,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 matching stock owners") + expect(page).to have_content("1 total stock owners") end context "when adding a stock owner" do @@ -637,7 +637,7 @@ RSpec.describe OrganisationRelationshipsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("1 matching agents") + expect(page).to have_content("1 total agents") end it "shows remove link(s)" do diff --git a/spec/requests/organisations_controller_spec.rb b/spec/requests/organisations_controller_spec.rb index 4bd6e0eb6..97665f8ca 100644 --- a/spec/requests/organisations_controller_spec.rb +++ b/spec/requests/organisations_controller_spec.rb @@ -59,6 +59,66 @@ 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_scheme) { create(:scheme, owning_organisation: specific_organisation) } + + before do + create_list(:scheme, 5, owning_organisation: specific_organisation) + create_list(:location, 3, scheme: specific_org_scheme) + get "/organisations/#{specific_organisation.id}/schemes", headers:, params: {} + end + + it "shows scheme and location download links" do + expect(page).to have_link("Download schemes (CSV)", href: schemes_csv_download_organisation_path(specific_organisation, download_type: "schemes")) + expect(page).to have_link("Download locations (CSV)", href: schemes_csv_download_organisation_path(specific_organisation, download_type: "locations")) + expect(page).to have_link("Download schemes and locations (CSV)", href: schemes_csv_download_organisation_path(specific_organisation, 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 schemes_csv_download_organisation_path(specific_organisation, 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 schemes_csv_download_organisation_path(specific_organisation, 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 schemes_csv_download_organisation_path(specific_organisation, 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 +176,62 @@ RSpec.describe OrganisationsController, type: :request do expect(page).to have_field("search", type: "search") end + describe "scheme and location csv downloads" do + before do + create_list(:scheme, 5, owning_organisation: user.organisation) + create_list(:location, 3, scheme: same_org_scheme) + end + + 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| @@ -415,7 +531,7 @@ RSpec.describe OrganisationsController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("#{user.organisation.users.count} matching users") + expect(page).to have_content("#{user.organisation.users.count} total users") end end @@ -799,7 +915,7 @@ RSpec.describe OrganisationsController, type: :request do total_number_of_orgs = Organisation.all.count expect(page).to have_link organisation.name, href: "organisations/#{organisation.id}/lettings-logs" expect(page).to have_link unauthorised_organisation.name, href: "organisations/#{unauthorised_organisation.id}/lettings-logs" - expect(page).to have_content("#{total_number_of_orgs} matching organisations") + expect(page).to have_content("#{total_number_of_orgs} total organisations") end it "shows a search bar" do @@ -828,7 +944,7 @@ RSpec.describe OrganisationsController, type: :request do end it "only shows logs for that organisation" do - expect(page).to have_content("#{total_number_of_org1_logs} matching logs") + expect(page).to have_content("#{total_number_of_org1_logs} total logs") organisation.lettings_logs.visible.map(&:id).each do |lettings_log_id| expect(page).to have_link lettings_log_id.to_s, href: "/lettings-logs/#{lettings_log_id}" @@ -982,7 +1098,7 @@ RSpec.describe OrganisationsController, type: :request do end it "only shows logs for that organisation" do - expect(page).to have_content("#{number_of_org1_sales_logs} matching logs") + expect(page).to have_content("#{number_of_org1_sales_logs} total logs") organisation.sales_logs.map(&:id).each do |sales_log_id| expect(page).to have_link sales_log_id.to_s, href: "/sales-logs/#{sales_log_id}" end @@ -1243,7 +1359,7 @@ RSpec.describe OrganisationsController, type: :request do end it "shows the total organisations count" do - expect(CGI.unescape_html(response.body)).to match("#{total_organisations_count} matching organisations") + expect(CGI.unescape_html(response.body)).to match("#{total_organisations_count} total organisations") end it "has pagination links" do diff --git a/spec/requests/sales_logs_controller_spec.rb b/spec/requests/sales_logs_controller_spec.rb index e3d5c9e0f..4b16d81ef 100644 --- a/spec/requests/sales_logs_controller_spec.rb +++ b/spec/requests/sales_logs_controller_spec.rb @@ -703,7 +703,7 @@ RSpec.describe SalesLogsController, type: :request do end it "shows the total log count" do - expect(CGI.unescape_html(response.body)).to match("1 matching logs") + expect(CGI.unescape_html(response.body)).to match("1 total logs") end it "does not show the pagination links" do @@ -756,7 +756,7 @@ RSpec.describe SalesLogsController, type: :request do end it "shows the total log count" do - expect(CGI.unescape_html(response.body)).to match("26 matching logs") + expect(CGI.unescape_html(response.body)).to match("26 total logs") end it "has pagination links" do diff --git a/spec/requests/schemes_controller_spec.rb b/spec/requests/schemes_controller_spec.rb index 3016c3f4b..0abeb92ae 100644 --- a/spec/requests/schemes_controller_spec.rb +++ b/spec/requests/schemes_controller_spec.rb @@ -191,6 +191,67 @@ RSpec.describe SchemesController, type: :request do expect(page).to have_content("Schemes") end + describe "scheme and location csv downloads" do + let!(:same_org_scheme) { create(:scheme, owning_organisation: user.organisation) } + let!(:specific_organisation) { create(:organisation) } + let!(:specific_org_scheme) { create(:scheme, owning_organisation: specific_organisation) } + + before do + create(:location, scheme: same_org_scheme) + create_list(:scheme, 5, owning_organisation: specific_organisation) + create_list(:location, 3, scheme: specific_org_scheme) + end + + 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 any organisation" do + before do + Scheme.destroy_all + get "/schemes" + 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 12 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 9 locations from 12 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 12 schemes with 9 locations.") + end + end + end + it "shows all schemes" do schemes.each do |scheme| expect(page).to have_content(scheme.id_to_display) @@ -236,7 +297,7 @@ RSpec.describe SchemesController, type: :request do end it "shows the total organisations count" do - expect(CGI.unescape_html(response.body)).to match("#{schemes.count} matching schemes") + expect(CGI.unescape_html(response.body)).to match("#{schemes.count} total schemes") end context "when paginating over 20 results" do @@ -252,7 +313,7 @@ RSpec.describe SchemesController, type: :request do end it "shows the total schemes count" do - expect(CGI.unescape_html(response.body)).to match("#{total_schemes_count} matching schemes") + expect(CGI.unescape_html(response.body)).to match("#{total_schemes_count} total schemes") end it "shows which schemes are being shown on the current page" do @@ -277,7 +338,7 @@ RSpec.describe SchemesController, type: :request do end it "shows the total schemes count" do - expect(CGI.unescape_html(response.body)).to match("#{total_schemes_count} matching schemes") + expect(CGI.unescape_html(response.body)).to match("#{total_schemes_count} total schemes") end it "has pagination links" do diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index aad0b9f28..7f1b3ffef 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -1194,7 +1194,7 @@ RSpec.describe UsersController, type: :request do end it "shows the pagination count" do - expect(page).to have_content("4 matching users") + expect(page).to have_content("4 total users") end it "shows the download csv link" do diff --git a/spec/services/csv/scheme_csv_service_spec.rb b/spec/services/csv/scheme_csv_service_spec.rb new file mode 100644 index 000000000..681cacc3a --- /dev/null +++ b/spec/services/csv/scheme_csv_service_spec.rb @@ -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 diff --git a/spec/services/filter_manager_spec.rb b/spec/services/filter_manager_spec.rb index f24ad1cf2..78923d7be 100644 --- a/spec/services/filter_manager_spec.rb +++ b/spec/services/filter_manager_spec.rb @@ -77,4 +77,21 @@ describe FilterManager do end end end + + describe "filter_schemes" do + let(:schemes) { create_list(:scheme, 5) } + let(:alphabetical_order_schemes) { [schemes[4], schemes[2], schemes[0], schemes[1], schemes[3]] } + + before do + schemes[4].update!(service_name: "a") + schemes[2].update!(service_name: "bB") + schemes[0].update!(service_name: "C") + schemes[1].update!(service_name: "Dd") + schemes[3].update!(service_name: "e") + end + + it "returns schemes in alphabetical order by service name" do + expect(described_class.filter_schemes(Scheme.all, nil, {}, nil, nil)).to eq(alphabetical_order_schemes) + end + end end