From 932e766cf82c58cb91759c31813cad60c44a5de4 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:42:44 +0100 Subject: [PATCH] CLDC-3518 Add collection resources page (#2673) * Dynamically display collection resources for years * Display mandatory resources on collection resources page * Move download resources to collection resources controller and refactor routes * Put managing resources button behind a feature flag * Everyone should be able to download mandatory resources * Remove mentions of legacy template and update bu path methods * Update styling * Update tests and routes * CLDC-3518 Allow updating mandatory collection resources (#2676) * Add update resource page * Allow updating mandatory collection resources * CLDC-3518 Add file validation to collection resources (#2678) * Add file validation to collection resources * Move some tests to feature tests * Extract validations into translations file * Rebase changes * More rebase updates * Read file * Update tests --- .../collection_resources_controller.rb | 112 +++++++ app/controllers/start_controller.rb | 67 +--- app/helpers/collection_resources_helper.rb | 47 +++ app/models/collection_resource.rb | 10 + .../forms/bulk_upload_lettings/guidance.rb | 8 - .../bulk_upload_lettings/prepare_your_file.rb | 21 +- .../forms/bulk_upload_sales/guidance.rb | 8 - .../bulk_upload_sales/prepare_your_file.rb | 21 +- app/services/collection_resources_service.rb | 8 + app/services/feature_toggle.rb | 8 + .../mandatory_collection_resources_service.rb | 56 ++++ app/services/storage/s3_service.rb | 7 + .../forms/prepare_your_file_2023.html.erb | 4 - .../forms/prepare_your_file_2023.html.erb | 1 - .../bulk_upload_shared/guidance.html.erb | 2 - .../_collection_resource_summary_list.erb | 32 ++ app/views/collection_resources/edit.html.erb | 28 ++ app/views/collection_resources/index.html.erb | 21 ++ .../layouts/_collection_resources.html.erb | 117 +------ config/locales/en.yml | 8 + config/routes.rb | 25 +- spec/factories/collection_resource.rb | 10 + spec/features/collection_resources_spec.rb | 168 ++++++++++ spec/fixtures/files/excel_file.xlsx | Bin 0 -> 8884 bytes spec/fixtures/files/pdf_file.pdf | Bin 0 -> 11871 bytes .../collection_resources_helper_spec.rb | 132 ++++++++ .../collection_resources_controller_spec.rb | 302 ++++++++++++++++++ spec/requests/start_controller_spec.rb | 24 +- .../collection_resources_service_spec.rb | 19 ++ ...atory_collection_resources_service_spec.rb | 42 +++ 30 files changed, 1058 insertions(+), 250 deletions(-) create mode 100644 app/controllers/collection_resources_controller.rb create mode 100644 app/models/collection_resource.rb create mode 100644 app/services/mandatory_collection_resources_service.rb create mode 100644 app/views/collection_resources/_collection_resource_summary_list.erb create mode 100644 app/views/collection_resources/edit.html.erb create mode 100644 app/views/collection_resources/index.html.erb create mode 100644 spec/factories/collection_resource.rb create mode 100644 spec/features/collection_resources_spec.rb create mode 100644 spec/fixtures/files/excel_file.xlsx create mode 100644 spec/fixtures/files/pdf_file.pdf create mode 100644 spec/requests/collection_resources_controller_spec.rb create mode 100644 spec/services/collection_resources_service_spec.rb create mode 100644 spec/services/mandatory_collection_resources_service_spec.rb diff --git a/app/controllers/collection_resources_controller.rb b/app/controllers/collection_resources_controller.rb new file mode 100644 index 000000000..f1fd6de07 --- /dev/null +++ b/app/controllers/collection_resources_controller.rb @@ -0,0 +1,112 @@ +class CollectionResourcesController < ApplicationController + include CollectionResourcesHelper + + before_action :authenticate_user!, except: %i[download_mandatory_collection_resource] + + def index + render_not_found unless current_user.support? + + @mandatory_lettings_collection_resources_per_year = MandatoryCollectionResourcesService.generate_resources("lettings", editable_collection_resource_years) + @mandatory_sales_collection_resources_per_year = MandatoryCollectionResourcesService.generate_resources("sales", editable_collection_resource_years) + end + + def download_mandatory_collection_resource + log_type = params[:log_type] + year = params[:year].to_i + resource_type = params[:resource_type] + + return render_not_found unless resource_for_year_can_be_downloaded?(year) + + resource = MandatoryCollectionResourcesService.generate_resource(log_type, year, resource_type) + return render_not_found unless resource + + download_resource(resource.download_filename) + end + + def edit + return render_not_found unless current_user.support? + + year = params[:year].to_i + resource_type = params[:resource_type] + log_type = params[:log_type] + + return render_not_found unless resource_for_year_can_be_updated?(year) + + @collection_resource = MandatoryCollectionResourcesService.generate_resource(log_type, year, resource_type) + + return render_not_found unless @collection_resource + + render "collection_resources/edit" + end + + def update + return render_not_found unless current_user.support? + + year = resource_params[:year].to_i + resource_type = resource_params[:resource_type] + log_type = resource_params[:log_type] + file = resource_params[:file] + + return render_not_found unless resource_for_year_can_be_updated?(year) + + @collection_resource = MandatoryCollectionResourcesService.generate_resource(log_type, year, resource_type) + render_not_found unless @collection_resource + + validate_file(file) + + return render "collection_resources/edit" if @collection_resource.errors.any? + + filename = @collection_resource.download_filename + begin + CollectionResourcesService.new.upload_collection_resource(filename, file) + rescue StandardError + @collection_resource.errors.add(:file, :error_uploading) + return render "collection_resources/edit" + end + + flash[:notice] = "The #{log_type} #{text_year_range_format(year)} #{@collection_resource.short_display_name.downcase} has been updated" + redirect_to collection_resources_path + end + +private + + def resource_params + params.require(:collection_resource).permit(:year, :log_type, :resource_type, :file) + end + + def download_resource(filename) + file = CollectionResourcesService.new.get_file(filename) + return render_not_found unless file + + send_data(file, disposition: "attachment", filename:) + end + + def resource_for_year_can_be_downloaded?(year) + return true if current_user&.support? && editable_collection_resource_years.include?(year) + + displayed_collection_resource_years.include?(year) + end + + def resource_for_year_can_be_updated?(year) + editable_collection_resource_years.include?(year) + end + + def validate_file(file) + return @collection_resource.errors.add(:file, :blank) unless file + return @collection_resource.errors.add(:file, :above_100_mb) if file.size > 100.megabytes + + argv = %W[file --brief --mime-type -- #{file.path}] + output = `#{argv.shelljoin}` + + case @collection_resource.resource_type + when "paper_form" + unless output.match?(/application\/pdf/) + @collection_resource.errors.add(:file, :must_be_pdf) + end + when "bulk_upload_template", "bulk_upload_specification" + unless output.match?(/application\/vnd\.ms-excel|application\/vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet/) + @collection_resource.errors.add(:file, :must_be_xlsx, resource: @collection_resource.short_display_name.downcase) + end + end + end +end diff --git a/app/controllers/start_controller.rb b/app/controllers/start_controller.rb index dd4232b7b..5bd49df3f 100644 --- a/app/controllers/start_controller.rb +++ b/app/controllers/start_controller.rb @@ -2,74 +2,11 @@ class StartController < ApplicationController include CollectionResourcesHelper def index + @mandatory_lettings_collection_resources_per_year = MandatoryCollectionResourcesService.generate_resources("lettings", displayed_collection_resource_years) + @mandatory_sales_collection_resources_per_year = MandatoryCollectionResourcesService.generate_resources("sales", displayed_collection_resource_years) if current_user @homepage_presenter = HomepagePresenter.new(current_user) render "home/index" end end - - def download_24_25_sales_form - download_resource("2024_25_sales_paper_form.pdf", "2024-25 Sales paper form.pdf") - end - - def download_23_24_sales_form - download_resource("2023_24_sales_paper_form.pdf", "2023-24 Sales paper form.pdf") - end - - def download_24_25_lettings_form - download_resource("2024_25_lettings_paper_form.pdf", "2024-25 Lettings paper form.pdf") - end - - def download_23_24_lettings_form - download_resource("2023_24_lettings_paper_form.pdf", "2023-24 Lettings paper form.pdf") - end - - def download_24_25_lettings_bulk_upload_template - download_resource("bulk-upload-lettings-template-2024-25.xlsx", "2024-25-lettings-bulk-upload-template.xlsx") - end - - def download_24_25_lettings_bulk_upload_specification - download_resource("bulk-upload-lettings-specification-2024-25.xlsx", "2024-25-lettings-bulk-upload-specification.xlsx") - end - - def download_24_25_sales_bulk_upload_template - download_resource("bulk-upload-sales-template-2024-25.xlsx", "2024-25-sales-bulk-upload-template.xlsx") - end - - def download_24_25_sales_bulk_upload_specification - download_resource("bulk-upload-sales-specification-2024-25.xlsx", "2024-25-sales-bulk-upload-specification.xlsx") - end - - def download_23_24_lettings_bulk_upload_template - download_resource("bulk-upload-lettings-template-2023-24.xlsx", "2023-24-lettings-bulk-upload-template.xlsx") - end - - def download_23_24_lettings_bulk_upload_legacy_template - download_resource("bulk-upload-lettings-legacy-template-2023-24.xlsx", "2023-24-lettings-bulk-upload-legacy-template.xlsx") - end - - def download_23_24_lettings_bulk_upload_specification - download_resource("bulk-upload-lettings-specification-2023-24.xlsx", "2023-24-lettings-bulk-upload-specification.xlsx") - end - - def download_23_24_sales_bulk_upload_template - download_resource("bulk-upload-sales-template-2023-24.xlsx", "2023-24-sales-bulk-upload-template.xlsx") - end - - def download_23_24_sales_bulk_upload_legacy_template - download_resource("bulk-upload-sales-legacy-template-2023-24.xlsx", "2023-24-sales-bulk-upload-legacy-template.xlsx") - end - - def download_23_24_sales_bulk_upload_specification - download_resource("bulk-upload-sales-specification-2023-24.xlsx", "2023-24-sales-bulk-upload-specification.xlsx") - end - -private - - def download_resource(filename, download_filename) - file = CollectionResourcesService.new.get_file(filename) - return render_not_found unless file - - send_data(file, disposition: "attachment", filename: download_filename) - end end diff --git a/app/helpers/collection_resources_helper.rb b/app/helpers/collection_resources_helper.rb index 5ab539cde..e1fb6cf2b 100644 --- a/app/helpers/collection_resources_helper.rb +++ b/app/helpers/collection_resources_helper.rb @@ -1,4 +1,6 @@ module CollectionResourcesHelper + include CollectionTimeHelper + HUMAN_READABLE_CONTENT_TYPE = { "application/pdf": "PDF", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Microsoft Excel", "application/vnd.ms-excel": "Microsoft Excel (Old Format)", @@ -19,4 +21,49 @@ module CollectionResourcesHelper file_type = HUMAN_READABLE_CONTENT_TYPE[metadata["content_type"].to_sym] || "Unknown File Type" [file_type, file_size, file_pages].compact.join(", ") end + + def displayed_collection_resource_years + return [previous_collection_start_year, current_collection_start_year] if FormHandler.instance.in_edit_crossover_period? + + [current_collection_start_year] + end + + def editable_collection_resource_years + return [previous_collection_start_year, current_collection_start_year] if FormHandler.instance.in_edit_crossover_period? + return [next_collection_start_year, current_collection_start_year] if (Time.zone.today >= Time.zone.local(Time.zone.today.year, 1, 1) && Time.zone.today < Time.zone.local(Time.zone.today.year, 4, 1)) || FeatureToggle.allow_future_resource_updates? + + [current_collection_start_year] + end + + def year_range_format(year) + "#{year % 100}/#{(year + 1) % 100}" + end + + def text_year_range_format(year) + "#{year} to #{year + 1}" + end + + def document_list_component_items(resources) + resources.map do |resource| + { + name: "Download the #{resource.display_name}", + href: resource.download_path, + metadata: file_type_size_and_pages(resource.download_filename), + } + end + end + + def document_list_edit_component_items(resources) + resources.map do |resource| + { + name: resource.download_filename, + href: resource.download_path, + metadata: file_type_size_and_pages(resource.download_filename), + } + end + end + + def file_exists_on_s3?(file) + CollectionResourcesService.new.file_exists_on_s3?(file) + end end diff --git a/app/models/collection_resource.rb b/app/models/collection_resource.rb new file mode 100644 index 000000000..d5c3c895b --- /dev/null +++ b/app/models/collection_resource.rb @@ -0,0 +1,10 @@ +class CollectionResource + include ActiveModel::Model + include Rails.application.routes.url_helpers + + attr_accessor :resource_type, :display_name, :short_display_name, :year, :log_type, :download_filename, :file + + def download_path + download_mandatory_collection_resource_path(log_type:, year:, resource_type:) + end +end diff --git a/app/models/forms/bulk_upload_lettings/guidance.rb b/app/models/forms/bulk_upload_lettings/guidance.rb index 24bc531f2..dc0de3271 100644 --- a/app/models/forms/bulk_upload_lettings/guidance.rb +++ b/app/models/forms/bulk_upload_lettings/guidance.rb @@ -24,10 +24,6 @@ module Forms end end - def lettings_legacy_template_path - Forms::BulkUploadLettings::PrepareYourFile.new.legacy_template_path - end - def lettings_template_path Forms::BulkUploadLettings::PrepareYourFile.new(year:).template_path end @@ -36,10 +32,6 @@ module Forms Forms::BulkUploadLettings::PrepareYourFile.new(year:).specification_path end - def sales_legacy_template_path - Forms::BulkUploadSales::PrepareYourFile.new.legacy_template_path - end - def sales_template_path Forms::BulkUploadSales::PrepareYourFile.new(year:).template_path end diff --git a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb index 984451dbb..b66c1649f 100644 --- a/app/models/forms/bulk_upload_lettings/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_lettings/prepare_your_file.rb @@ -32,29 +32,12 @@ module Forms bulk_upload_lettings_log_path(id: "upload-your-file", form: { year:, needstype:, organisation_id: }.compact) end - def legacy_template_path - case year - when 2023 - download_23_24_lettings_bulk_upload_legacy_template_path - end - end - def template_path - case year - when 2023 - download_23_24_lettings_bulk_upload_template_path - when 2024 - download_24_25_lettings_bulk_upload_template_path - end + download_mandatory_collection_resource_path(year:, log_type: "lettings", resource_type: "bulk_upload_template") end def specification_path - case year - when 2023 - download_23_24_lettings_bulk_upload_specification_path - when 2024 - download_24_25_lettings_bulk_upload_specification_path - end + download_mandatory_collection_resource_path(year:, log_type: "lettings", resource_type: "bulk_upload_specification") end def year_combo diff --git a/app/models/forms/bulk_upload_sales/guidance.rb b/app/models/forms/bulk_upload_sales/guidance.rb index ef792a3e4..be61ef650 100644 --- a/app/models/forms/bulk_upload_sales/guidance.rb +++ b/app/models/forms/bulk_upload_sales/guidance.rb @@ -24,10 +24,6 @@ module Forms end end - def lettings_legacy_template_path - Forms::BulkUploadLettings::PrepareYourFile.new.legacy_template_path - end - def lettings_template_path Forms::BulkUploadLettings::PrepareYourFile.new(year:).template_path end @@ -36,10 +32,6 @@ module Forms Forms::BulkUploadLettings::PrepareYourFile.new(year:).specification_path end - def sales_legacy_template_path - Forms::BulkUploadSales::PrepareYourFile.new.legacy_template_path - end - def sales_template_path Forms::BulkUploadSales::PrepareYourFile.new(year:).template_path end diff --git a/app/models/forms/bulk_upload_sales/prepare_your_file.rb b/app/models/forms/bulk_upload_sales/prepare_your_file.rb index d6d5276c2..6bac024cc 100644 --- a/app/models/forms/bulk_upload_sales/prepare_your_file.rb +++ b/app/models/forms/bulk_upload_sales/prepare_your_file.rb @@ -31,29 +31,12 @@ module Forms bulk_upload_sales_log_path(id: "upload-your-file", form: { year:, organisation_id: }.compact) end - def legacy_template_path - case year - when 2023 - download_23_24_sales_bulk_upload_legacy_template_path - end - end - def template_path - case year - when 2023 - download_23_24_sales_bulk_upload_template_path - when 2024 - download_24_25_sales_bulk_upload_template_path - end + download_mandatory_collection_resource_path(year:, log_type: "sales", resource_type: "bulk_upload_template") end def specification_path - case year - when 2023 - download_23_24_sales_bulk_upload_specification_path - when 2024 - download_24_25_sales_bulk_upload_specification_path - end + download_mandatory_collection_resource_path(year:, log_type: "sales", resource_type: "bulk_upload_specification") end def year_combo diff --git a/app/services/collection_resources_service.rb b/app/services/collection_resources_service.rb index f347a4ebf..81ab08254 100644 --- a/app/services/collection_resources_service.rb +++ b/app/services/collection_resources_service.rb @@ -18,4 +18,12 @@ class CollectionResourcesService rescue StandardError nil end + + def file_exists_on_s3?(file) + @storage_service.file_exists?(file) + end + + def upload_collection_resource(filename, file) + @storage_service.write_file(filename, file) + end end diff --git a/app/services/feature_toggle.rb b/app/services/feature_toggle.rb index f63eceaef..5312d1964 100644 --- a/app/services/feature_toggle.rb +++ b/app/services/feature_toggle.rb @@ -38,4 +38,12 @@ class FeatureToggle def self.local_storage? Rails.env.development? end + + def self.allow_future_resource_updates? + !Rails.env.production? && !Rails.env.test? + end + + def self.managing_resources_enabled? + !Rails.env.production? + end end diff --git a/app/services/mandatory_collection_resources_service.rb b/app/services/mandatory_collection_resources_service.rb new file mode 100644 index 000000000..82a6bd41e --- /dev/null +++ b/app/services/mandatory_collection_resources_service.rb @@ -0,0 +1,56 @@ +class MandatoryCollectionResourcesService + MANDATORY_RESOURCES = %w[paper_form bulk_upload_template bulk_upload_specification].freeze + + def self.generate_resources(log_type, collection_years) + mandatory_resources_per_year = {} + collection_years.map do |year| + mandatory_resources_per_year[year] = resources_per_year(year, log_type) + end + mandatory_resources_per_year + end + + def self.resources_per_year(year, log_type) + MANDATORY_RESOURCES.map do |resource| + generate_resource(log_type, year, resource) + end + end + + def self.generate_resource(log_type, year, resource_type) + return unless log_type && year && resource_type + return unless %w[lettings sales].include?(log_type) + return unless MANDATORY_RESOURCES.include?(resource_type) + + CollectionResource.new( + resource_type:, + display_name: display_name(resource_type, year, log_type), + short_display_name: resource_type.humanize, + year:, + log_type:, + download_filename: download_filename(resource_type, year, log_type), + ) + end + + def self.display_name(resource, year, log_type) + year_range = "#{year} to #{year + 1}" + case resource + when "paper_form" + "#{log_type} log for tenants (#{year_range})" + when "bulk_upload_template" + "#{log_type} bulk upload template (#{year_range})" + when "bulk_upload_specification" + "#{log_type} bulk upload specification (#{year_range})" + end + end + + def self.download_filename(resource, year, log_type) + year_range = "#{year}_#{(year + 1) % 100}" + case resource + when "paper_form" + "#{year_range}_#{log_type}_paper_form.pdf" + when "bulk_upload_template" + "bulk-upload-#{log_type}-template-#{year_range.dasherize}.xlsx" + when "bulk_upload_specification" + "bulk-upload-#{log_type}-specification-#{year_range.dasherize}.xlsx" + end + end +end diff --git a/app/services/storage/s3_service.rb b/app/services/storage/s3_service.rb index 3329b2ea1..88199c0a0 100644 --- a/app/services/storage/s3_service.rb +++ b/app/services/storage/s3_service.rb @@ -48,6 +48,13 @@ module Storage @client.head_object(bucket: @configuration.bucket_name, key: file_name) end + def file_exists?(file_name) + @client.head_object(bucket: @configuration.bucket_name, key: file_name) + true + rescue Aws::S3::Errors::NotFound + false + end + private def create_configuration diff --git a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2023.html.erb b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2023.html.erb index 21d19dba8..a07092278 100644 --- a/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2023.html.erb +++ b/app/views/bulk_upload_lettings_logs/forms/prepare_your_file_2023.html.erb @@ -18,10 +18,6 @@
  • <%= govuk_link_to "Download the new template", @form.template_path %>: In this template, the questions are in the same order as the 2023/24 paper form and web form.
  • - -
  • - <%= govuk_link_to "Download the legacy template", @form.legacy_template_path %>: In this template, the questions are in the same order as the 2022/23 template, with new questions added on to the end. -
  • There are 7 or 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.

    diff --git a/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2023.html.erb b/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2023.html.erb index b9d0990be..427a835e8 100644 --- a/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2023.html.erb +++ b/app/views/bulk_upload_sales_logs/forms/prepare_your_file_2023.html.erb @@ -16,7 +16,6 @@

    Use one of these templates to upload logs for 2023/24:

    There are 7 or 8 rows of content in the templates. These rows are called the ‘headers’. They contain the CORE form questions and guidance about which questions are required and how to format your answers.

    diff --git a/app/views/bulk_upload_shared/guidance.html.erb b/app/views/bulk_upload_shared/guidance.html.erb index e530aa5b5..4cb8b76b4 100644 --- a/app/views/bulk_upload_shared/guidance.html.erb +++ b/app/views/bulk_upload_shared/guidance.html.erb @@ -52,8 +52,6 @@

    <%= govuk_link_to "Download the sales bulk upload template (2023 to 2024) – New question ordering", @form.sales_template_path %>

    Legacy template: In this template, the questions are in the same order as the 2022/23 template, with new questions added on to the end. Use this template if you have not updated your system to match the new template yet.

    -

    <%= govuk_link_to "Download the lettings bulk upload template (2023 to 2024) - Legacy version", @form.lettings_legacy_template_path %>

    -

    <%= govuk_link_to "Download the sales bulk upload template (2023 to 2024) – Legacy version", @form.sales_legacy_template_path %>

    <% else %>

    <%= govuk_link_to "Download the lettings bulk upload template (2024 to 2025)", @form.lettings_template_path %>

    <%= govuk_link_to "Download the sales bulk upload template (2024 to 2025)", @form.sales_template_path %>

    diff --git a/app/views/collection_resources/_collection_resource_summary_list.erb b/app/views/collection_resources/_collection_resource_summary_list.erb new file mode 100644 index 000000000..61c29865d --- /dev/null +++ b/app/views/collection_resources/_collection_resource_summary_list.erb @@ -0,0 +1,32 @@ +
    +
    + <%= govuk_summary_list do |summary_list| %> + <% mandatory_resources.each do |resource| %> + <% summary_list.with_row do |row| %> + <% row.with_key { resource.short_display_name } %> + <% if file_exists_on_s3?(resource.download_filename) %> + <% row.with_value do %> + <%= render DocumentListComponent.new(items: document_list_edit_component_items([resource]), label: "") %> + <% end %> + <% row.with_action( + text: "Change", + href: edit_mandatory_collection_resource_path(year: resource.year, log_type: resource.log_type, resource_type: resource.resource_type), + ) %> + <% else %> + <% row.with_value do %> +

    No file uploaded

    + <% end %> + <% row.with_action( + text: "Upload", + href: "/", + ) %> + <% end %> + <% end %> + <% end %> + <% end %> +
    + <%= govuk_link_to "Add new #{mandatory_resources.first.log_type} #{text_year_range_format(mandatory_resources.first.year)} resource", href: "/" %> +
    +
    +
    +
    diff --git a/app/views/collection_resources/edit.html.erb b/app/views/collection_resources/edit.html.erb new file mode 100644 index 000000000..d9bfe5be9 --- /dev/null +++ b/app/views/collection_resources/edit.html.erb @@ -0,0 +1,28 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: collection_resources_path %> +<% end %> + +
    +
    + <%= form_with model: @collection_resource, url: update_mandatory_collection_resource_path, method: :patch do |f| %> + <%= f.hidden_field :year %> + <%= f.hidden_field :log_type %> + <%= f.hidden_field :resource_type %> + + <%= f.govuk_error_summary %> + + <%= "#{@collection_resource.log_type.humanize} #{text_year_range_format(@collection_resource.year)}" %> +

    Change the <%= @collection_resource.resource_type.humanize.downcase %>

    + +

    + This file will be available for all users to download. +

    + + <%= f.govuk_file_field :file, + label: { text: "Upload file", size: "m" } %> + + <%= f.govuk_submit "Save changes" %> + <%= govuk_button_link_to "Cancel", collection_resources_path, secondary: true %> + <% end %> +
    +
    diff --git a/app/views/collection_resources/index.html.erb b/app/views/collection_resources/index.html.erb new file mode 100644 index 000000000..f6a999e59 --- /dev/null +++ b/app/views/collection_resources/index.html.erb @@ -0,0 +1,21 @@ +<% title = "Collection resources" %> +<% content_for :title, title %> +<% content_for :before_content do %> + <%= govuk_back_link(href: :back) %> +<% end %> + +

    <%= title %>

    + +<% @mandatory_lettings_collection_resources_per_year.each do |year, mandatory_resources| %> +

    + Lettings <%= text_year_range_format(year) %> +

    + <%= render partial: "collection_resource_summary_list", locals: { mandatory_resources: } %> +<% end %> + +<% @mandatory_sales_collection_resources_per_year.each do |year, mandatory_resources| %> +

    + Sales <%= text_year_range_format(year) %> +

    + <%= render partial: "collection_resource_summary_list", locals: { mandatory_resources: } %> +<% end %> diff --git a/app/views/layouts/_collection_resources.html.erb b/app/views/layouts/_collection_resources.html.erb index 48976088a..935f1806c 100644 --- a/app/views/layouts/_collection_resources.html.erb +++ b/app/views/layouts/_collection_resources.html.erb @@ -5,113 +5,22 @@ <% else %>

    Collection resources

    <% end %> -

    Use the 2024 to 2025 forms for lettings that start and sales that complete between 1 April 2024 and 31 March 2025.

    -<% if FormHandler.instance.lettings_form_for_start_year(2023) && FormHandler.instance.lettings_form_for_start_year(2023).edit_end_date > Time.zone.today %> -

    Use the 2023 to 2024 forms for lettings that start and sales that complete between 1 April 2023 and 31 March 2024.

    +<% displayed_collection_resource_years.each do |collection_start_year| %> +

    Use the <%= collection_start_year %> to <%= collection_start_year + 1 %> forms for lettings that start and sales that complete between 1 April <%= collection_start_year %> and 31 March <%= collection_start_year + 1 %>.

    <% end %>
    -<%= govuk_tabs(title: "Collection resources", classes: %w[app-tab__small-headers]) do |c| %> - <% if FormHandler.instance.lettings_form_for_start_year(2024) && FormHandler.instance.lettings_form_for_start_year(2024).edit_end_date > Time.zone.today %> - <% c.with_tab(label: "Lettings 24/25") do %> - <%= render DocumentListComponent.new( - items: [ - { - name: "Download the lettings log for tenants (2024 to 2025)", - href: download_24_25_lettings_form_path, - metadata: file_type_size_and_pages("2024_25_lettings_paper_form.pdf", number_of_pages: 8), - }, - { - name: "Download the lettings bulk upload template (2024 to 2025)", - href: download_24_25_lettings_bulk_upload_template_path, - metadata: file_type_size_and_pages("bulk-upload-lettings-template-2024-25.xlsx"), - }, - { - name: "Download the lettings bulk upload specification (2024 to 2025)", - href: download_24_25_lettings_bulk_upload_specification_path, - metadata: file_type_size_and_pages("bulk-upload-lettings-specification-2024-25.xlsx"), - }, - ], - label: "Lettings 2024 to 2025", - ) %> + <%= govuk_tabs(title: "Collection resources", classes: %w[app-tab__small-headers]) do |c| %> + <% @mandatory_lettings_collection_resources_per_year.each do |year, resources| %> + <% c.with_tab(label: "Lettings #{year_range_format(year)}") do %> + <%= render DocumentListComponent.new(items: document_list_component_items(resources), label: "Lettings #{text_year_range_format(year)}") %> + <% end %> <% end %> - <% c.with_tab(label: "Sales 24/25") do %> - <%= render DocumentListComponent.new( - items: [ - { - name: "Download the sales log for buyers (2024 to 2025)", - href: download_24_25_sales_form_path, - metadata: file_type_size_and_pages("2024_25_sales_paper_form.pdf", number_of_pages: 8), - }, - { - name: "Download the sales bulk upload template (2024 to 2025)", - href: download_24_25_sales_bulk_upload_template_path, - metadata: file_type_size_and_pages("bulk-upload-sales-template-2024-25.xlsx"), - }, - { - name: "Download the sales bulk upload specification (2024 to 2025)", - href: download_24_25_sales_bulk_upload_specification_path, - metadata: file_type_size_and_pages("bulk-upload-sales-specification-2024-25.xlsx"), - }, - ], - label: "Sales 2024 to 2025", - ) %> + <% @mandatory_sales_collection_resources_per_year.each do |year, resources| %> + <% c.with_tab(label: "Sales #{year_range_format(year)}") do %> + <%= render DocumentListComponent.new(items: document_list_component_items(resources), label: "Sales #{text_year_range_format(year)}") %> + <% end %> <% end %> <% end %> - <% if FormHandler.instance.lettings_form_for_start_year(2023) && FormHandler.instance.lettings_form_for_start_year(2023).edit_end_date > Time.zone.today %> - <% c.with_tab(label: "Lettings 23/24") do %> - <%= render DocumentListComponent.new( - items: [ - { - name: "Download the lettings log for tenants (2023 to 2024)", - href: download_23_24_lettings_form_path, - metadata: file_type_size_and_pages("2023_24_lettings_paper_form.pdf", number_of_pages: 8), - }, - { - name: "Download the lettings bulk upload template (2023 to 2024) – New question ordering", - href: download_23_24_lettings_bulk_upload_template_path, - metadata: file_type_size_and_pages("bulk-upload-lettings-template-2023-24.xlsx"), - }, - { - name: "Download the lettings bulk upload template (2023 to 2024) – Legacy version", - href: download_23_24_lettings_bulk_upload_legacy_template_path, - metadata: file_type_size_and_pages("bulk-upload-lettings-legacy-template-2023-24.xlsx"), - }, - { - name: "Download the lettings bulk upload specification (2023 to 2024)", - href: download_23_24_lettings_bulk_upload_specification_path, - metadata: file_type_size_and_pages("bulk-upload-lettings-specification-2023-24.xlsx"), - }, - ], - label: "Lettings 2023 to 2024", - ) %> - <% end %> - <% c.with_tab(label: "Sales 23/24") do %> - <%= render DocumentListComponent.new( - items: [ - { - name: "Download the sales log for buyers (2023 to 2024)", - href: download_23_24_sales_form_path, - metadata: file_type_size_and_pages("2023_24_sales_paper_form.pdf", number_of_pages: 8), - }, - { - name: "Download the sales bulk upload template (2023 to 2024) – New question ordering", - href: download_23_24_sales_bulk_upload_template_path, - metadata: file_type_size_and_pages("bulk-upload-sales-template-2023-24.xlsx"), - }, - { - name: "Download the sales bulk upload template (2023 to 2024) – Legacy version", - href: download_23_24_sales_bulk_upload_legacy_template_path, - metadata: file_type_size_and_pages("bulk-upload-sales-legacy-template-2023-24.xlsx"), - }, - { - name: "Download the sales bulk upload specification (2023 to 2024)", - href: download_23_24_sales_bulk_upload_specification_path, - metadata: file_type_size_and_pages("bulk-upload-sales-specification-2023-24.xlsx"), - }, - ], - label: "Sales 2023 to 2024", - ) %> - <% end %> - <% end %> -<% end %>
    + +<%= govuk_button_link_to "Manage collection resources", collection_resources_path, secondary: true, class: "govuk-!-margin-bottom-2" if current_user&.support? && FeatureToggle.managing_resources_enabled? %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 81d059269..d7f27a54e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -117,6 +117,14 @@ en: attributes: confirm_soft_errors: blank: "You must select if there are errors in these fields." + collection_resource: + attributes: + file: + error_uploading: There was an error uploading this file. + blank: Select which file to upload. + above_100_mb: The file is above 100MB. + must_be_pdf: The paper form must be a PDF. + must_be_xlsx: The %{resource} must be a Microsoft Excel file. activerecord: attributes: diff --git a/config/routes.rb b/config/routes.rb index 7ba2dabed..3fa83458a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,23 +40,14 @@ Rails.application.routes.draw do get "/service-moved", to: "maintenance#service_moved" get "/service-unavailable", to: "maintenance#service_unavailable" - get "/download-23-24-lettings-form", to: "start#download_23_24_lettings_form" - get "/download-23-24-lettings-bulk-upload-template", to: "start#download_23_24_lettings_bulk_upload_template" - get "/download-23-24-lettings-bulk-upload-legacy-template", to: "start#download_23_24_lettings_bulk_upload_legacy_template" - get "/download-23-24-lettings-bulk-upload-specification", to: "start#download_23_24_lettings_bulk_upload_specification" - - get "/download-23-24-sales-form", to: "start#download_23_24_sales_form" - get "/download-23-24-sales-bulk-upload-template", to: "start#download_23_24_sales_bulk_upload_template" - get "/download-23-24-sales-bulk-upload-legacy-template", to: "start#download_23_24_sales_bulk_upload_legacy_template" - get "/download-23-24-sales-bulk-upload-specification", to: "start#download_23_24_sales_bulk_upload_specification" - - get "/download-24-25-lettings-form", to: "start#download_24_25_lettings_form" - get "/download-24-25-lettings-bulk-upload-template", to: "start#download_24_25_lettings_bulk_upload_template" - get "/download-24-25-lettings-bulk-upload-specification", to: "start#download_24_25_lettings_bulk_upload_specification" - - get "/download-24-25-sales-form", to: "start#download_24_25_sales_form" - get "/download-24-25-sales-bulk-upload-template", to: "start#download_24_25_sales_bulk_upload_template" - get "/download-24-25-sales-bulk-upload-specification", to: "start#download_24_25_sales_bulk_upload_specification" + get "collection-resources", to: "collection_resources#index" + get "/collection-resources/:log_type/:year/:resource_type/download", to: "collection_resources#download_mandatory_collection_resource", as: :download_mandatory_collection_resource + get "/collection-resources/:log_type/:year/:resource_type/edit", to: "collection_resources#edit", as: :edit_mandatory_collection_resource + patch "/collection-resources", to: "collection_resources#update", as: :update_mandatory_collection_resource + + resources :collection_resources, path: "/collection-resources" do + get "/download", to: "collection_resources#download_additional_collection_resource" # when we get to adding them + end get "clear-filters", to: "sessions#clear_filters" diff --git a/spec/factories/collection_resource.rb b/spec/factories/collection_resource.rb new file mode 100644 index 000000000..c352b70a9 --- /dev/null +++ b/spec/factories/collection_resource.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :collection_resource, class: "CollectionResource" do + resource_type { "paper_form" } + display_name { "lettings log for tenants (2021 to 2022)" } + short_display_name { "Paper Form" } + year { 2024 } + log_type { "lettings" } + download_filename { "24_25_lettings_paper_form.pdf" } + end +end diff --git a/spec/features/collection_resources_spec.rb b/spec/features/collection_resources_spec.rb new file mode 100644 index 000000000..643f3537e --- /dev/null +++ b/spec/features/collection_resources_spec.rb @@ -0,0 +1,168 @@ +require "rails_helper" + +RSpec.describe "Collection resources" do + let(:user) { create(:user, :support) } + let(:collection_resources_service) { instance_double(CollectionResourcesService, file_exists_on_s3?: true) } + + before do + allow(CollectionResourcesService).to receive(:new).and_return(collection_resources_service) + allow(collection_resources_service).to receive(:upload_collection_resource) + allow(collection_resources_service).to receive(:get_file_metadata).and_return({ "content_type" => "application/pdf", "content_length" => 1000 }) + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + end + + context "when uploading paper form" do + it "only allows pdf files for lettings" do + visit("/collection-resources/lettings/2024/paper_form/edit") + + click_button("Save changes") + + expect(page).to have_content("Select which file to upload") + + expect(page).to have_content("Change the paper form") + expect(page).to have_content("Lettings 2024 to 2025") + + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).to have_content("The paper form must be a PDF.") + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Save changes") + + expect(page).not_to have_content("The paper form must be a PDF.") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("2024_25_lettings_paper_form.pdf", anything) + expect(page).to have_content("The lettings 2024 to 2025 paper form has been updated") + end + + it "only allows pdf files for sales" do + visit("/collection-resources/sales/2024/paper_form/edit") + + click_button("Save changes") + + expect(page).to have_content("Select which file to upload") + + expect(page).to have_content("Change the paper form") + expect(page).to have_content("Sales 2024 to 2025") + + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).to have_content("The paper form must be a PDF.") + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Save changes") + + expect(page).not_to have_content("The paper form must be a PDF.") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("2024_25_sales_paper_form.pdf", anything) + expect(page).to have_content("The sales 2024 to 2025 paper form has been updated") + end + end + + context "when uploading bu template" do + it "only allows excel files for lettings" do + visit("/collection-resources/lettings/2024/bulk_upload_template/edit") + + click_button("Save changes") + + expect(page).to have_content("Select which file to upload") + + expect(page).to have_content("Change the bulk upload template") + expect(page).to have_content("Lettings 2024 to 2025") + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Save changes") + + expect(page).to have_content("The bulk upload template must be a Microsoft Excel file.") + + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).not_to have_content("The bulk upload template must be a Microsoft Excel file.") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("bulk-upload-lettings-template-2024-25.xlsx", anything) + expect(page).to have_content("The lettings 2024 to 2025 bulk upload template has been updated") + end + + it "only allows excel files for sales" do + visit("/collection-resources/sales/2024/bulk_upload_template/edit") + + click_button("Save changes") + + expect(page).to have_content("Select which file to upload") + + expect(page).to have_content("Change the bulk upload template") + expect(page).to have_content("Sales 2024 to 2025") + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Save changes") + + expect(page).to have_content("The bulk upload template must be a Microsoft Excel file.") + + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).not_to have_content("The bulk upload template must be a Microsoft Excel file.") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("bulk-upload-sales-template-2024-25.xlsx", anything) + expect(page).to have_content("The sales 2024 to 2025 bulk upload template has been updated") + end + end + + context "when uploading bu specification" do + it "only allows excel files for lettings" do + visit("/collection-resources/lettings/2024/bulk_upload_specification/edit") + + click_button("Save changes") + + expect(page).to have_content("Select which file to upload") + + expect(page).to have_content("Change the bulk upload specification") + expect(page).to have_content("Lettings 2024 to 2025") + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Save changes") + + expect(page).to have_content("The bulk upload specification must be a Microsoft Excel file.") + + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).not_to have_content("The bulk upload specification must be a Microsoft Excel file.") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("bulk-upload-lettings-specification-2024-25.xlsx", anything) + expect(page).to have_content("The lettings 2024 to 2025 bulk upload specification has been updated") + end + + it "only allows excel files for sales" do + visit("/collection-resources/sales/2024/bulk_upload_specification/edit") + + click_button("Save changes") + + expect(page).to have_content("Select which file to upload") + + expect(page).to have_content("Change the bulk upload specification") + expect(page).to have_content("Sales 2024 to 2025") + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Save changes") + + expect(page).to have_content("The bulk upload specification must be a Microsoft Excel file.") + + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).not_to have_content("The bulk upload specification must be a Microsoft Excel file.") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("bulk-upload-sales-specification-2024-25.xlsx", anything) + expect(page).to have_content("The sales 2024 to 2025 bulk upload specification has been updated") + end + + it "displays error message if the upload fails" do + allow(collection_resources_service).to receive(:upload_collection_resource).and_raise(StandardError) + + visit("/collection-resources/sales/2024/bulk_upload_specification/edit") + attach_file "file", file_fixture("excel_file.xlsx") + click_button("Save changes") + + expect(page).to have_content("There was an error uploading this file.") + end + end +end diff --git a/spec/fixtures/files/excel_file.xlsx b/spec/fixtures/files/excel_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..dcc5aaaf34a86215de60383dc5b8bece6aaa0abe GIT binary patch literal 8884 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 literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/pdf_file.pdf b/spec/fixtures/files/pdf_file.pdf new file mode 100644 index 0000000000000000000000000000000000000000..65a01d0a996c342b3b07ed73877d437004eb45d0 GIT binary patch literal 11871 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 literal 0 HcmV?d00001 diff --git a/spec/helpers/collection_resources_helper_spec.rb b/spec/helpers/collection_resources_helper_spec.rb index 4d39a0c2d..31033d997 100644 --- a/spec/helpers/collection_resources_helper_spec.rb +++ b/spec/helpers/collection_resources_helper_spec.rb @@ -31,4 +31,136 @@ RSpec.describe CollectionResourcesHelper do end end end + + describe "#editable_collection_resource_years" do + context "when in crossover period" do + before do + allow(FormHandler.instance).to receive(:in_edit_crossover_period?).and_return(true) + allow(Time.zone).to receive(:today).and_return(Time.zone.local(2024, 4, 8)) + end + + it "returns previous and current years" do + expect(editable_collection_resource_years).to eq([2023, 2024]) + end + end + + context "when not in crossover period" do + before do + allow(FormHandler.instance).to receive(:in_edit_crossover_period?).and_return(false) + end + + context "and after 1st January" do + before do + allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 2, 1)) + end + + it "returns current and next years" do + expect(editable_collection_resource_years).to match_array([2024, 2025]) + end + end + + context "and before 1st January" do + before do + allow(Time.zone).to receive(:today).and_return(Time.zone.local(2024, 12, 1)) + end + + it "returns current year" do + expect(editable_collection_resource_years).to eq([2024]) + end + end + end + end + + describe "#displayed_collection_resource_years" do + context "when in crossover period" do + before do + allow(FormHandler.instance).to receive(:in_edit_crossover_period?).and_return(true) + allow(Time.zone).to receive(:today).and_return(Time.zone.local(2024, 4, 8)) + end + + it "returns previous and current years" do + expect(displayed_collection_resource_years).to eq([2023, 2024]) + end + end + + context "when not in crossover period" do + before do + allow(FormHandler.instance).to receive(:in_edit_crossover_period?).and_return(false) + end + + it "returns current year" do + expect(displayed_collection_resource_years).to eq([2024]) + end + end + end + + describe "#year_range_format" do + it "returns formatted year range" do + expect(year_range_format(2023)).to eq("23/24") + end + end + + describe "#text_year_range_format" do + it "returns formatted text year range" do + expect(text_year_range_format(2023)).to eq("2023 to 2024") + end + end + + describe "#document_list_component_items" do + let(:resources) do + [ + build(:collection_resource, year: 2023, resource_type: "paper_form", display_name: "lettings log for tenants (2023 to 2024)", download_filename: "2023_24_lettings_paper_form.pdf"), + build(:collection_resource, year: 2023, resource_type: "bulk_upload_template", display_name: "bulk upload template (2023 to 2024)", download_filename: "2023_24_lettings_bulk_upload_template.xlsx"), + ] + end + + before do + allow(storage_service).to receive(:get_file_metadata).with("2023_24_lettings_paper_form.pdf").and_return("content_length" => 292_864, "content_type" => "application/pdf") + allow(storage_service).to receive(:get_file_metadata).with("2023_24_lettings_bulk_upload_template.xlsx").and_return("content_length" => 19_456, "content_type" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + end + + it "returns component items" do + expect(document_list_component_items(resources)).to eq([ + { + name: "Download the lettings log for tenants (2023 to 2024)", + href: "/collection-resources/lettings/2023/paper_form/download", + metadata: "PDF, 286 KB", + }, + { + name: "Download the bulk upload template (2023 to 2024)", + href: "/collection-resources/lettings/2023/bulk_upload_template/download", + metadata: "Microsoft Excel, 19 KB", + }, + ]) + end + end + + describe "#document_list_edit_component_items" do + let(:resources) do + [ + build(:collection_resource, year: 2023, resource_type: "paper_form", display_name: "lettings log for tenants (2023 to 2024)", download_filename: "2023_24_lettings_paper_form.pdf"), + build(:collection_resource, year: 2023, resource_type: "bulk_upload_template", display_name: "bulk upload template (2023 to 2024)", download_filename: "2023_24_lettings_bulk_upload_template.xlsx"), + ] + end + + before do + allow(storage_service).to receive(:get_file_metadata).with("2023_24_lettings_paper_form.pdf").and_return("content_length" => 292_864, "content_type" => "application/pdf") + allow(storage_service).to receive(:get_file_metadata).with("2023_24_lettings_bulk_upload_template.xlsx").and_return("content_length" => 19_456, "content_type" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + end + + it "returns component items" do + expect(document_list_edit_component_items(resources)).to eq([ + { + name: "2023_24_lettings_paper_form.pdf", + href: "/collection-resources/lettings/2023/paper_form/download", + metadata: "PDF, 286 KB", + }, + { + name: "2023_24_lettings_bulk_upload_template.xlsx", + href: "/collection-resources/lettings/2023/bulk_upload_template/download", + metadata: "Microsoft Excel, 19 KB", + }, + ]) + end + end end diff --git a/spec/requests/collection_resources_controller_spec.rb b/spec/requests/collection_resources_controller_spec.rb new file mode 100644 index 000000000..597f70a83 --- /dev/null +++ b/spec/requests/collection_resources_controller_spec.rb @@ -0,0 +1,302 @@ +require "rails_helper" + +RSpec.describe CollectionResourcesController, type: :request do + let(:page) { Capybara::Node::Simple.new(response.body) } + let(:storage_service) { instance_double(Storage::S3Service, get_file_metadata: nil) } + + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:configuration).and_return(OpenStruct.new(bucket_name: "core-test-collection-resources")) + end + + describe "GET #index" do + context "when user is not signed in" do + it "redirects to the sign in page" do + get collection_resources_path + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when user is signed in as a data coordinator" do + let(:user) { create(:user, :data_coordinator) } + + before do + sign_in user + end + + it "returns page not found" do + get collection_resources_path + expect(response).to have_http_status(:not_found) + end + end + + context "when user is signed in as a data provider" do + let(:user) { create(:user, :data_provider) } + + before do + sign_in user + end + + it "returns page not found" do + get collection_resources_path + expect(response).to have_http_status(:not_found) + end + end + + context "when user is signed in as a support user" do + let(:user) { create(:user, :support) } + + before do + allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 1, 8)) + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + allow(storage_service).to receive(:file_exists?).and_return(true) + sign_in user + end + + it "displays collection resources" do + get collection_resources_path + + expect(page).to have_content("Lettings 2024 to 2025") + expect(page).to have_content("Lettings 2025 to 2026") + expect(page).to have_content("Sales 2024 to 2025") + expect(page).to have_content("Sales 2025 to 2026") + end + + it "displays mandatory filed" do + get collection_resources_path + + expect(page).to have_content("Paper form") + expect(page).to have_content("Bulk upload template") + expect(page).to have_content("Bulk upload specification") + end + + context "when files are on S3" do + before do + allow(storage_service).to receive(:file_exists?).and_return(true) + get collection_resources_path + end + + it "displays file names with download links" do + expect(page).to have_link("2024_25_lettings_paper_form.pdf", href: download_mandatory_collection_resource_path(year: 2024, log_type: "lettings", resource_type: "paper_form")) + expect(page).to have_link("bulk-upload-lettings-template-2024-25.xlsx", href: download_mandatory_collection_resource_path(year: 2024, log_type: "lettings", resource_type: "bulk_upload_template")) + expect(page).to have_link("bulk-upload-lettings-specification-2024-25.xlsx", href: download_mandatory_collection_resource_path(year: 2024, log_type: "lettings", resource_type: "bulk_upload_specification")) + expect(page).to have_link("2024_25_sales_paper_form.pdf", href: download_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "paper_form")) + expect(page).to have_link("bulk-upload-sales-template-2024-25.xlsx", href: download_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template")) + expect(page).to have_link("bulk-upload-sales-specification-2024-25.xlsx", href: download_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_specification")) + + expect(page).to have_link("2025_26_lettings_paper_form.pdf", href: download_mandatory_collection_resource_path(year: 2025, log_type: "lettings", resource_type: "paper_form")) + expect(page).to have_link("bulk-upload-lettings-template-2025-26.xlsx", href: download_mandatory_collection_resource_path(year: 2025, log_type: "lettings", resource_type: "bulk_upload_template")) + expect(page).to have_link("bulk-upload-lettings-specification-2025-26.xlsx", href: download_mandatory_collection_resource_path(year: 2025, log_type: "lettings", resource_type: "bulk_upload_specification")) + expect(page).to have_link("2025_26_sales_paper_form.pdf", href: download_mandatory_collection_resource_path(year: 2025, log_type: "sales", resource_type: "paper_form")) + expect(page).to have_link("bulk-upload-sales-template-2025-26.xlsx", href: download_mandatory_collection_resource_path(year: 2025, log_type: "sales", resource_type: "bulk_upload_template")) + expect(page).to have_link("bulk-upload-sales-specification-2025-26.xlsx", href: download_mandatory_collection_resource_path(year: 2025, log_type: "sales", resource_type: "bulk_upload_specification")) + end + + it "displays change links" do + expect(page).to have_selector(:link_or_button, "Change", count: 12) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2024, log_type: "lettings", resource_type: "paper_form")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2024, log_type: "lettings", resource_type: "bulk_upload_template")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2024, log_type: "lettings", resource_type: "bulk_upload_specification")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "paper_form")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_specification")) + + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2025, log_type: "lettings", resource_type: "paper_form")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2025, log_type: "lettings", resource_type: "bulk_upload_template")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2025, log_type: "lettings", resource_type: "bulk_upload_specification")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2025, log_type: "sales", resource_type: "paper_form")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2025, log_type: "sales", resource_type: "bulk_upload_template")) + expect(page).to have_link("Change", href: edit_mandatory_collection_resource_path(year: 2025, log_type: "sales", resource_type: "bulk_upload_specification")) + end + end + + context "when files are not on S3" do + before do + allow(storage_service).to receive(:file_exists?).and_return(false) + get collection_resources_path + end + + it "displays No file uploaded" do + expect(page).to have_content("No file uploaded") + end + + it "displays upload links" do + expect(page).to have_selector(:link_or_button, "Upload", count: 12) + end + end + end + end + + describe "GET #download_mandatory_collection_resource" do + before do + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(CollectionResourcesHelper).to receive(:editable_collection_resource_years).and_return([2025, 2026]) + allow_any_instance_of(CollectionResourcesHelper).to receive(:displayed_collection_resource_years).and_return([2025]) + # rubocop:enable RSpec/AnyInstance + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + end + + context "when user is signed in as a data coordinator" do + let(:user) { create(:user, :data_coordinator) } + + context "when the file exists on S3" do + before do + allow(storage_service).to receive(:get_file).and_return("file") + get download_mandatory_collection_resource_path(log_type: "lettings", year: 2025, resource_type: "paper_form") + end + + it "downloads the file" do + expect(response.body).to eq("file") + end + end + + context "when the file does not exist on S3" do + before do + allow(storage_service).to receive(:get_file).and_return(nil) + get download_mandatory_collection_resource_path(log_type: "lettings", year: 2024, resource_type: "paper_form") + end + + it "returns page not found" do + expect(response).to have_http_status(:not_found) + end + end + + context "when resource isn't a mandatory resources" do + before do + get download_mandatory_collection_resource_path(log_type: "lettings", year: 2024, resource_type: "invalid_resource") + end + + it "returns page not found" do + expect(response).to have_http_status(:not_found) + end + end + + context "when year not in displayed_collection_resource_years" do + before do + get download_mandatory_collection_resource_path(log_type: "lettings", year: 2026, resource_type: "paper_form") + end + + it "returns page not found" do + expect(response).to have_http_status(:not_found) + end + end + end + + context "when user is signed in as a support user" do + let(:user) { create(:user, :support) } + + context "when year is in editable_collection_resource_years but not in displayed_collection_resource_years" do + before do + allow(storage_service).to receive(:get_file).and_return("file") + get download_mandatory_collection_resource_path(log_type: "lettings", year: 2026, resource_type: "paper_form") + end + + it "downloads the file" do + expect(response.status).to eq(200) + expect(response.body).to eq("file") + end + end + end + end + + describe "GET #edit_mandatory_collection_resource" do + context "when user is not signed in" do + it "redirects to the sign in page" do + get edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template") + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when user is signed in as a data coordinator" do + let(:user) { create(:user, :data_coordinator) } + + before do + sign_in user + end + + it "returns page not found" do + get edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template") + expect(response).to have_http_status(:not_found) + end + end + + context "when user is signed in as a data provider" do + let(:user) { create(:user, :data_provider) } + + before do + sign_in user + end + + it "returns page not found" do + get edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template") + expect(response).to have_http_status(:not_found) + end + end + + context "when user is signed in as a support user" do + let(:user) { create(:user, :support) } + + before do + allow(Time.zone).to receive(:today).and_return(Time.zone.local(2025, 1, 8)) + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + end + + it "displays update collection resources page content" do + get edit_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template") + + expect(page).to have_content("Sales 2024 to 2025") + expect(page).to have_content("Change the bulk upload template") + expect(page).to have_content("This file will be available for all users to download.") + expect(page).to have_content("Upload file") + expect(page).to have_button("Save changes") + expect(page).to have_link("Back", href: collection_resources_path) + expect(page).to have_link("Cancel", href: collection_resources_path) + end + end + end + + describe "PATCH #update_mandatory_collection_resource" do + let(:some_file) { File.open(file_fixture("blank_bulk_upload_sales.csv")) } + let(:params) { { collection_resource: { year: 2024, log_type: "sales", resource_type: "bulk_upload_template", file: some_file } } } + let(:collection_resource_service) { instance_double(CollectionResourcesService) } + + before do + allow(CollectionResourcesService).to receive(:new).and_return(collection_resource_service) + end + + context "when user is not signed in" do + it "redirects to the sign in page" do + patch update_mandatory_collection_resource_path(year: 2024, log_type: "sales", resource_type: "bulk_upload_template", file: some_file) + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when user is signed in as a data coordinator" do + let(:user) { create(:user, :data_coordinator) } + + before do + sign_in user + end + + it "returns page not found" do + patch update_mandatory_collection_resource_path, params: params + expect(response).to have_http_status(:not_found) + end + end + + context "when user is signed in as a data provider" do + let(:user) { create(:user, :data_provider) } + + before do + sign_in user + end + + it "returns page not found" do + patch update_mandatory_collection_resource_path, params: params + expect(response).to have_http_status(:not_found) + end + end + end +end diff --git a/spec/requests/start_controller_spec.rb b/spec/requests/start_controller_spec.rb index 40c45a9f9..db884d09f 100644 --- a/spec/requests/start_controller_spec.rb +++ b/spec/requests/start_controller_spec.rb @@ -324,7 +324,7 @@ RSpec.describe StartController, type: :request do context "and 2023 collection window is open for editing" do before do - allow(Time).to receive(:now).and_return(Time.zone.local(2024, 1, 1)) + allow(Time).to receive(:now).and_return(Time.zone.local(2024, 4, 1)) end it "displays correct resources for 2023/24 and 2024/25 collection years" do @@ -342,10 +342,10 @@ RSpec.describe StartController, type: :request do context "and 2023 collection window is closed for editing" do before do - allow(Time).to receive(:now).and_return(Time.zone.local(2025, 1, 1)) + allow(Time).to receive(:now).and_return(Time.zone.local(2024, 12, 1)) end - it "displays correct resources for 2023/24 and 2024/25 collection years" do + it "displays correct resources" do get root_path expect(page).to have_content("Lettings 24/25") expect(page).not_to have_content("Lettings 23/24") @@ -367,6 +367,24 @@ RSpec.describe StartController, type: :request do get root_path expect(page).to have_content("About this service") end + + context "with support user" do + let(:user) { create(:user, :support) } + + it "displays link to edit collection resources" do + get root_path + + expect(page).to have_link("Manage collection resources", href: collection_resources_path) + end + end + + context "with data coordinator" do + it "does not display the link to edit collection resources" do + get root_path + + expect(page).not_to have_link("Manage collection resources", href: collection_resources_path) + end + end end end diff --git a/spec/services/collection_resources_service_spec.rb b/spec/services/collection_resources_service_spec.rb new file mode 100644 index 000000000..3786b70c9 --- /dev/null +++ b/spec/services/collection_resources_service_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" + +describe CollectionResourcesService do + let(:service) { described_class.new } + let(:some_file) { File.open(file_fixture("blank_bulk_upload_sales.csv")) } + let(:storage_service) { instance_double(Storage::S3Service) } + + describe "#upload_collection_resource" do + before do + allow(Storage::S3Service).to receive(:new).and_return(storage_service) + allow(storage_service).to receive(:write_file) + end + + it "calls write_file on S3 service" do + expect(storage_service).to receive(:write_file).with("2025_26_lettings_paper_form.pdf", some_file) + service.upload_collection_resource("2025_26_lettings_paper_form.pdf", some_file) + end + end +end diff --git a/spec/services/mandatory_collection_resources_service_spec.rb b/spec/services/mandatory_collection_resources_service_spec.rb new file mode 100644 index 000000000..423370e96 --- /dev/null +++ b/spec/services/mandatory_collection_resources_service_spec.rb @@ -0,0 +1,42 @@ +require "rails_helper" + +describe MandatoryCollectionResourcesService do + let(:service) { described_class } + + describe "#generate_resource" do + it "returns a CollectionResource object" do + resource = service.generate_resource("lettings", 2024, "paper_form") + expect(resource).to be_a(CollectionResource) + end + + it "returns nil if resource type is not in the MANDATORY_RESOURCES list" do + resource = service.generate_resource("lettings", 2024, "invalid_resource") + expect(resource).to be_nil + end + + it "returns a CollectionResource object with the correct attributes" do + resource = service.generate_resource("lettings", 2024, "paper_form") + expect(resource.resource_type).to eq("paper_form") + expect(resource.display_name).to eq("lettings log for tenants (2024 to 2025)") + expect(resource.short_display_name).to eq("Paper form") + expect(resource.year).to eq(2024) + expect(resource.log_type).to eq("lettings") + expect(resource.download_filename).to eq("2024_25_lettings_paper_form.pdf") + end + end + + describe "#generate_resources" do + it "generates all mandatory resources for given years" do + resources = service.generate_resources("lettings", [2024, 2025]) + expect(resources[2024].map(&:resource_type)).to eq(%w[paper_form bulk_upload_template bulk_upload_specification]) + expect(resources[2025].map(&:resource_type)).to eq(%w[paper_form bulk_upload_template bulk_upload_specification]) + end + end + + describe "#resources_per_year" do + it "generates all mandatory resources for a specific year" do + resources = service.resources_per_year(2024, "lettings") + expect(resources.map(&:resource_type)).to eq(%w[paper_form bulk_upload_template bulk_upload_specification]) + end + end +end