From e71b2aeb9f687c9ac41d2b3e5993d5867a6f4690 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:21:41 +0100 Subject: [PATCH] CLDC-3638 Allow adding custom resources (#2690) * Stub some collection resource requests * Rebase changes * Add upload new resource page * Allow adding custom collection resources * Display and validate additional resources * Allow downloading additional collection resources * Allow updating additional resources * Display additional resources on the homepage * Add delete link to additional resources * Update hint text * Update tests * Allow unauthenticated users to donwload additional collection resources * Add some changes to copy and styling * Add content type to the files * Validate short display name at the same time as file --- .../collection_resources_controller.rb | 122 +++++-- app/controllers/start_controller.rb | 2 + app/helpers/collection_resources_helper.rb | 2 +- app/models/collection_resource.rb | 31 +- app/services/collection_resources_service.rb | 3 +- .../mandatory_collection_resources_service.rb | 1 + app/services/storage/local_disk_service.rb | 2 +- app/services/storage/s3_service.rb | 21 +- .../_collection_resource_summary_list.erb | 19 +- app/views/collection_resources/edit.html.erb | 21 +- app/views/collection_resources/index.html.erb | 4 +- app/views/collection_resources/new.html.erb | 36 ++ .../layouts/_collection_resources.html.erb | 4 +- config/locales/en.yml | 7 +- config/routes.rb | 8 +- spec/factories/collection_resource.rb | 10 + spec/features/collection_resources_spec.rb | 73 ++++ .../collection_resources_helper_spec.rb | 12 +- .../collection_resources_controller_spec.rb | 342 +++++++++++++++++- spec/requests/start_controller_spec.rb | 2 + .../collection_resources_service_spec.rb | 2 +- 21 files changed, 668 insertions(+), 56 deletions(-) create mode 100644 app/views/collection_resources/new.html.erb diff --git a/app/controllers/collection_resources_controller.rb b/app/controllers/collection_resources_controller.rb index ed080c749..5203ceea1 100644 --- a/app/controllers/collection_resources_controller.rb +++ b/app/controllers/collection_resources_controller.rb @@ -1,13 +1,15 @@ class CollectionResourcesController < ApplicationController include CollectionResourcesHelper - before_action :authenticate_user!, except: %i[download_mandatory_collection_resource] + before_action :authenticate_user!, except: %i[download_mandatory_collection_resource download_additional_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) + @additional_lettings_collection_resources_per_year = CollectionResource.where(log_type: "lettings", mandatory: false).group_by(&:year) + @additional_sales_collection_resources_per_year = CollectionResource.where(log_type: "sales", mandatory: false).group_by(&:year) end def download_mandatory_collection_resource @@ -23,7 +25,16 @@ class CollectionResourcesController < ApplicationController download_resource(resource.download_filename) end - def edit + def download_additional_collection_resource + resource = CollectionResource.find_by(id: params[:collection_resource_id]) + + return render_not_found unless resource + return render_not_found unless resource_for_year_can_be_downloaded?(resource.year) + + download_resource(resource.download_filename) + end + + def edit_mandatory_collection_resource return render_not_found unless current_user.support? year = params[:year].to_i @@ -39,7 +50,18 @@ class CollectionResourcesController < ApplicationController render "collection_resources/edit" end - def update + def edit_additional_collection_resource + return render_not_found unless current_user.support? + + @collection_resource = CollectionResource.find_by(id: params[:collection_resource_id]) + + return render_not_found unless @collection_resource + return render_not_found unless resource_for_year_can_be_updated?(@collection_resource.year) + + render "collection_resources/edit" + end + + def update_mandatory_collection_resource return render_not_found unless current_user.support? year = resource_params[:year].to_i @@ -52,7 +74,8 @@ class CollectionResourcesController < ApplicationController @collection_resource = MandatoryCollectionResourcesService.generate_resource(log_type, year, resource_type) render_not_found unless @collection_resource - validate_file(file) + @collection_resource.file = file + @collection_resource.validate_attached_file return render "collection_resources/edit" if @collection_resource.errors.any? @@ -68,6 +91,36 @@ class CollectionResourcesController < ApplicationController redirect_to collection_resources_path end + def update_additional_collection_resource + return render_not_found unless current_user.support? + + @collection_resource = CollectionResource.find_by(id: params[:collection_resource_id]) + + return render_not_found unless @collection_resource + return render_not_found unless resource_for_year_can_be_updated?(@collection_resource.year) + + @collection_resource.file = resource_params[:file] + @collection_resource.validate_attached_file + @collection_resource.validate_short_display_name + return render "collection_resources/edit" if @collection_resource.errors.any? + + @collection_resource.short_display_name = resource_params[:short_display_name] + @collection_resource.download_filename = @collection_resource.file&.original_filename + @collection_resource.display_name = "#{@collection_resource.log_type} #{@collection_resource.short_display_name} (#{text_year_range_format(@collection_resource.year)})" + if @collection_resource.save + begin + CollectionResourcesService.new.upload_collection_resource(@collection_resource.download_filename, @collection_resource.file) + flash[:notice] = "The #{@collection_resource.log_type} #{text_year_range_format(@collection_resource.year)} #{@collection_resource.short_display_name.downcase} has been updated." + redirect_to collection_resources_path + rescue StandardError + @collection_resource.errors.add(:file, :error_uploading) + render "collection_resources/edit" + end + else + render "collection_resources/edit" + end + end + def confirm_mandatory_collection_resources_release return render_not_found unless current_user.support? @@ -91,10 +144,50 @@ class CollectionResourcesController < ApplicationController redirect_to collection_resources_path end + def new + return render_not_found unless current_user.support? + + year = params[:year].to_i + log_type = params[:log_type] + + return render_not_found unless editable_collection_resource_years.include?(year) + + @collection_resource = CollectionResource.new(year:, log_type:) + end + + def create + return render_not_found unless current_user.support? && editable_collection_resource_years.include?(resource_params[:year].to_i) + + @collection_resource = CollectionResource.new(resource_params) + @collection_resource.download_filename ||= @collection_resource.file&.original_filename + @collection_resource.display_name = "#{@collection_resource.log_type} #{@collection_resource.short_display_name} (#{text_year_range_format(@collection_resource.year)})" + + @collection_resource.validate_attached_file + @collection_resource.validate_short_display_name + return render "collection_resources/new" if @collection_resource.errors.any? + + if @collection_resource.save + begin + CollectionResourcesService.new.upload_collection_resource(@collection_resource.download_filename, @collection_resource.file) + flash[:notice] = if displayed_collection_resource_years.include?(@collection_resource.year) + "The #{@collection_resource.log_type} #{text_year_range_format(@collection_resource.year)} #{@collection_resource.short_display_name} is now available to users." + else + "The #{@collection_resource.log_type} #{text_year_range_format(@collection_resource.year)} #{@collection_resource.short_display_name} has been uploaded." + end + redirect_to collection_resources_path + rescue StandardError + @collection_resource.errors.add(:file, :error_uploading) + render "collection_resources/new" + end + else + render "collection_resources/new" + end + end + private def resource_params - params.require(:collection_resource).permit(:year, :log_type, :resource_type, :file) + params.require(:collection_resource).permit(:year, :log_type, :resource_type, :file, :mandatory, :short_display_name) end def download_resource(filename) @@ -113,23 +206,4 @@ private 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 5bd49df3f..b65da4d44 100644 --- a/app/controllers/start_controller.rb +++ b/app/controllers/start_controller.rb @@ -4,6 +4,8 @@ class StartController < ApplicationController 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) + @additional_lettings_collection_resources_per_year = CollectionResource.where(log_type: "lettings", mandatory: false, year: displayed_collection_resource_years).group_by(&:year) + @additional_sales_collection_resources_per_year = CollectionResource.where(log_type: "sales", mandatory: false, year: displayed_collection_resource_years).group_by(&:year) if current_user @homepage_presenter = HomepagePresenter.new(current_user) render "home/index" diff --git a/app/helpers/collection_resources_helper.rb b/app/helpers/collection_resources_helper.rb index 9deb13887..1a5887744 100644 --- a/app/helpers/collection_resources_helper.rb +++ b/app/helpers/collection_resources_helper.rb @@ -49,7 +49,7 @@ module CollectionResourcesHelper def document_list_component_items(resources) resources.map do |resource| { - name: "Download the #{resource.display_name}", + name: "Download the #{resource.display_name.downcase}", href: resource.download_path, metadata: file_type_size_and_pages(resource.download_filename), } diff --git a/app/models/collection_resource.rb b/app/models/collection_resource.rb index 81665ccf6..cc217b8cd 100644 --- a/app/models/collection_resource.rb +++ b/app/models/collection_resource.rb @@ -3,7 +3,36 @@ class CollectionResource < ApplicationRecord attr_accessor :file + validates :short_display_name, presence: true + def download_path - download_mandatory_collection_resource_path(log_type:, year:, resource_type:) + if mandatory + download_mandatory_collection_resource_path(log_type:, year:, resource_type:) + else + collection_resource_download_path(self) + end + end + + def validate_attached_file + return errors.add(:file, :blank) unless file + return errors.add(:file, :above_100_mb) if file.size > 100.megabytes + + argv = %W[file --brief --mime-type -- #{file.path}] + output = `#{argv.shelljoin}` + + case resource_type + when "paper_form" + unless output.match?(/application\/pdf/) + 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/) + errors.add(:file, :must_be_xlsx, resource: short_display_name.downcase) + end + end + end + + def validate_short_display_name + errors.add(:short_display_name, :blank) if short_display_name.blank? end end diff --git a/app/services/collection_resources_service.rb b/app/services/collection_resources_service.rb index 81ab08254..82247f7a0 100644 --- a/app/services/collection_resources_service.rb +++ b/app/services/collection_resources_service.rb @@ -24,6 +24,7 @@ class CollectionResourcesService end def upload_collection_resource(filename, file) - @storage_service.write_file(filename, file) + content_type = MiniMime.lookup_by_filename(filename)&.content_type + @storage_service.write_file(filename, file, content_type:) end end diff --git a/app/services/mandatory_collection_resources_service.rb b/app/services/mandatory_collection_resources_service.rb index 8b34153e9..397e4b5d0 100644 --- a/app/services/mandatory_collection_resources_service.rb +++ b/app/services/mandatory_collection_resources_service.rb @@ -27,6 +27,7 @@ class MandatoryCollectionResourcesService year:, log_type:, download_filename: download_filename(resource_type, year, log_type), + mandatory: true, ) end diff --git a/app/services/storage/local_disk_service.rb b/app/services/storage/local_disk_service.rb index bb5825340..ad3cc9608 100644 --- a/app/services/storage/local_disk_service.rb +++ b/app/services/storage/local_disk_service.rb @@ -19,7 +19,7 @@ module Storage File.open(path, "r") end - def write_file(filename, data) + def write_file(filename, data, _content_type: nil) path = Rails.root.join("tmp/storage", filename) FileUtils.mkdir_p(path.dirname) diff --git a/app/services/storage/s3_service.rb b/app/services/storage/s3_service.rb index 88199c0a0..2e8daa719 100644 --- a/app/services/storage/s3_service.rb +++ b/app/services/storage/s3_service.rb @@ -36,12 +36,21 @@ module Storage .body.read end - def write_file(file_name, data) - @client.put_object( - body: data, - bucket: @configuration.bucket_name, - key: file_name, - ) + def write_file(file_name, data, content_type: nil) + if content_type.nil? + @client.put_object( + body: data, + bucket: @configuration.bucket_name, + key: file_name, + ) + else + @client.put_object( + body: data, + bucket: @configuration.bucket_name, + key: file_name, + content_type:, + ) + end end def get_file_metadata(file_name) diff --git a/app/views/collection_resources/_collection_resource_summary_list.erb b/app/views/collection_resources/_collection_resource_summary_list.erb index 8ef588fd5..0e630f0aa 100644 --- a/app/views/collection_resources/_collection_resource_summary_list.erb +++ b/app/views/collection_resources/_collection_resource_summary_list.erb @@ -23,9 +23,26 @@ <% end %> <% end %> <% end %> + <% additional_resources&.each do |resource| %> + <% summary_list.with_row do |row| %> + <% row.with_key { resource.short_display_name } %> + <% row.with_value do %> + <%= render DocumentListComponent.new(items: document_list_edit_component_items([resource]), label: "") %> + <% end %> + <% row.with_action( + text: "Change", + href: collection_resource_edit_path(resource), + ) %> + <% row.with_action( + text: "Delete", + href: "/", + classes: "app-!-colour-red" + ) %> + <% end %> + <% end %> <% end %>
- <%= govuk_link_to "Add new #{mandatory_resources.first.log_type} #{text_year_range_format(mandatory_resources.first.year)} resource", href: "/" %> + <%= govuk_link_to "Add new #{mandatory_resources.first.log_type} #{text_year_range_format(mandatory_resources.first.year)} resource", href: new_collection_resource_path(year: mandatory_resources.first.year, log_type: mandatory_resources.first.log_type) %>

diff --git a/app/views/collection_resources/edit.html.erb b/app/views/collection_resources/edit.html.erb index e5b2a6d33..6100256c8 100644 --- a/app/views/collection_resources/edit.html.erb +++ b/app/views/collection_resources/edit.html.erb @@ -5,7 +5,7 @@
<% resource_exists = file_exists_on_s3?(@collection_resource.download_filename) %> - <%= form_with model: @collection_resource, url: update_mandatory_collection_resource_path, method: :patch do |f| %> + <%= form_with model: @collection_resource, url: @collection_resource.mandatory ? update_mandatory_collection_resource_path : collection_resource_update_path(@collection_resource), method: :patch do |f| %> <%= f.hidden_field :year %> <%= f.hidden_field :log_type %> <%= f.hidden_field :resource_type %> @@ -13,7 +13,7 @@ <%= f.govuk_error_summary %> <%= "#{@collection_resource.log_type.humanize} #{text_year_range_format(@collection_resource.year)}" %> -

<%= resource_exists ? "Change" : "Upload" %> the <%= @collection_resource.resource_type.humanize.downcase %>

+

<%= resource_exists ? "Change" : "Upload" %> the <%= @collection_resource.short_display_name.downcase %>

This file will be available for all users to download. @@ -21,9 +21,22 @@ <%= f.govuk_file_field :file, label: { text: "Upload file", size: "m" } %> + <% if resource_exists %> +

Current file: <%= govuk_link_to @collection_resource.download_filename, href: @collection_resource.download_path %>

+ <% end %> - <%= f.govuk_submit resource_exists ? "Save changes" : "Upload" %> - <%= govuk_button_link_to "Cancel", collection_resources_path, secondary: true %> + <% unless @collection_resource.mandatory %> + <%= f.govuk_text_field :short_display_name, + label: { text: "Resource type", size: "m" }, + hint: { text: safe_join(["This will be used in the download link on the homepage. Do not include the log type or collection year.", + content_tag(:br), + "For example, if you enter “bulk upload change log”, the download link will say “Download the #{@collection_resource.log_type} bulk upload change log (#{text_year_range_format(@collection_resource.year)})”."]) } %> + <% end %> + +
+ <%= f.govuk_submit resource_exists ? "Save changes" : "Upload" %> + <%= 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 index f6785eb46..207a63991 100644 --- a/app/views/collection_resources/index.html.erb +++ b/app/views/collection_resources/index.html.erb @@ -18,12 +18,12 @@

Lettings <%= text_year_range_format(year) %>

- <%= render partial: "collection_resource_summary_list", locals: { mandatory_resources: } %> + <%= render partial: "collection_resource_summary_list", locals: { mandatory_resources:, additional_resources: @additional_lettings_collection_resources_per_year[year] } %> <% 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: } %> + <%= render partial: "collection_resource_summary_list", locals: { mandatory_resources:, additional_resources: @additional_sales_collection_resources_per_year[year] } %> <% end %> diff --git a/app/views/collection_resources/new.html.erb b/app/views/collection_resources/new.html.erb new file mode 100644 index 000000000..f66c56b94 --- /dev/null +++ b/app/views/collection_resources/new.html.erb @@ -0,0 +1,36 @@ +<% content_for :before_content do %> + <%= govuk_back_link href: collection_resources_path %> +<% end %> + +
+
+ <%= form_with model: @collection_resource, url: collection_resources_path, method: :post do |f| %> + <%= f.hidden_field :year %> + <%= f.hidden_field :log_type %> + <%= f.hidden_field :mandatory, value: false %> + + <%= f.govuk_error_summary %> + + <%= "#{@collection_resource.log_type.humanize} #{text_year_range_format(@collection_resource.year)}" %> +

Add a new collection resource

+ +

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

+ + <%= f.govuk_file_field :file, + label: { text: "Upload file", size: "m" } %> + + <%= f.govuk_text_field :short_display_name, + label: { text: "Resource type", size: "m" }, + hint: { text: safe_join(["This will be used in the download link on the homepage. Do not include the log type or collection year.", + content_tag(:br), + "For example, if you enter “bulk upload change log”, the download link will say “Download the #{@collection_resource.log_type} bulk upload change log (#{text_year_range_format(@collection_resource.year)})”."]) } %> + +
+ <%= f.govuk_submit "Add resource" %> + <%= govuk_button_link_to "Cancel", collection_resources_path, secondary: true %> +
+ <% end %> +
+
diff --git a/app/views/layouts/_collection_resources.html.erb b/app/views/layouts/_collection_resources.html.erb index 935f1806c..07648e5c0 100644 --- a/app/views/layouts/_collection_resources.html.erb +++ b/app/views/layouts/_collection_resources.html.erb @@ -12,12 +12,12 @@ <%= 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)}") %> + <%= render DocumentListComponent.new(items: document_list_component_items(resources.concat(@additional_lettings_collection_resources_per_year[year] || [])), label: "Lettings #{text_year_range_format(year)}") %> <% end %> <% end %> <% @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)}") %> + <%= render DocumentListComponent.new(items: document_list_component_items(resources.concat(@additional_sales_collection_resources_per_year[year] || [])), label: "Sales #{text_year_range_format(year)}") %> <% end %> <% end %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index ff73bfdcb..d490fd8ae 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -122,7 +122,7 @@ en: file: error_uploading: There was an error uploading this file. blank: Select which file to upload. - above_100_mb: The file is above 100MB. + above_100_mb: File must be 100MB or less. must_be_pdf: The paper form must be a PDF. must_be_xlsx: The %{resource} must be a Microsoft Excel file. @@ -212,9 +212,12 @@ en: file: error_uploading: There was an error uploading this file. blank: Select which file to upload. - above_100_mb: The file is above 100MB. + above_100_mb: File must be 100MB or less. must_be_pdf: The paper form must be a PDF. must_be_xlsx: The %{resource} must be a Microsoft Excel file. + short_display_name: + blank: "You must answer resource type." + notification: logs_deleted: one: "%{count} log has been deleted." diff --git a/config/routes.rb b/config/routes.rb index db4b2a8a2..cbac4894a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,13 +42,15 @@ Rails.application.routes.draw do 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 + get "/collection-resources/:log_type/:year/:resource_type/edit", to: "collection_resources#edit_mandatory_collection_resource", as: :edit_mandatory_collection_resource + patch "/collection-resources", to: "collection_resources#update_mandatory_collection_resource", as: :update_mandatory_collection_resource get "/collection-resources/:year/release", to: "collection_resources#confirm_mandatory_collection_resources_release", as: :confirm_mandatory_collection_resources_release patch "/collection-resources/:year/release", to: "collection_resources#release_mandatory_collection_resources", as: :release_mandatory_collection_resources resources :collection_resources, path: "/collection-resources" do - get "/download", to: "collection_resources#download_additional_collection_resource" # when we get to adding them + get "/download", to: "collection_resources#download_additional_collection_resource" + get "/edit", to: "collection_resources#edit_additional_collection_resource" + patch "/update", to: "collection_resources#update_additional_collection_resource" end get "clear-filters", to: "sessions#clear_filters" diff --git a/spec/factories/collection_resource.rb b/spec/factories/collection_resource.rb index c352b70a9..0282e33e3 100644 --- a/spec/factories/collection_resource.rb +++ b/spec/factories/collection_resource.rb @@ -6,5 +6,15 @@ FactoryBot.define do year { 2024 } log_type { "lettings" } download_filename { "24_25_lettings_paper_form.pdf" } + mandatory { true } + trait(:additional) do + resource_type { nil } + display_name { "lettings additional resource (2024 to 2025)" } + short_display_name { "additional resource" } + year { 2024 } + log_type { "lettings" } + download_filename { "additional.pdf" } + mandatory { false } + end end end diff --git a/spec/features/collection_resources_spec.rb b/spec/features/collection_resources_spec.rb index 643f3537e..ad9a370b6 100644 --- a/spec/features/collection_resources_spec.rb +++ b/spec/features/collection_resources_spec.rb @@ -5,6 +5,9 @@ RSpec.describe "Collection resources" do let(:collection_resources_service) { instance_double(CollectionResourcesService, file_exists_on_s3?: true) } before do + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(CollectionResourcesHelper).to receive(:editable_collection_resource_years).and_return([2024, 2025]) + # rubocop:enable RSpec/AnyInstance 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 }) @@ -165,4 +168,74 @@ RSpec.describe "Collection resources" do expect(page).to have_content("There was an error uploading this file.") end end + + context "when uploading an additional resource" do + it "allows valid files" do + expect(CollectionResource.count).to eq(0) + + visit(new_collection_resource_path(year: 2025, log_type: "sales")) + fill_in("collection_resource[short_display_name]", with: "some file") + attach_file "file", file_fixture("pdf_file.pdf") + + click_button("Add resource") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("pdf_file.pdf", anything) + expect(CollectionResource.count).to eq(1) + expect(CollectionResource.first.year).to eq(2025) + expect(CollectionResource.first.log_type).to eq("sales") + expect(CollectionResource.first.resource_type).to be_nil + expect(CollectionResource.first.mandatory).to be_falsey + expect(CollectionResource.first.released_to_user).to be_nil + expect(CollectionResource.first.display_name).to eq("sales some file (2025 to 2026)") + expect(CollectionResource.first.short_display_name).to eq("some file") + expect(page).to have_content("The sales 2025 to 2026 some file has been uploaded.") + end + + it "validates file is attached" do + visit(new_collection_resource_path(year: 2025, log_type: "sales")) + + fill_in("collection_resource[short_display_name]", with: "some file") + click_button("Add resource") + expect(page).to have_content("Select which file to upload.") + end + + it "validates resource type is given" do + visit(new_collection_resource_path(year: 2025, log_type: "sales")) + + attach_file "file", file_fixture("pdf_file.pdf") + click_button("Add resource") + expect(page).to have_content("You must answer resource type.") + end + end + + context "when updating an additional resource" do + let!(:collection_resource) { create(:collection_resource, :additional, year: 2025, log_type: "sales") } + + it "only allows valid files" do + expect(CollectionResource.count).to eq(1) + + visit(collection_resource_edit_path(collection_resource)) + fill_in("collection_resource[short_display_name]", with: "some updated file") + attach_file "file", file_fixture("pdf_file.pdf") + + click_button("Save changes") + expect(collection_resources_service).to have_received(:upload_collection_resource).with("pdf_file.pdf", anything) + expect(CollectionResource.count).to eq(1) + expect(CollectionResource.first.year).to eq(2025) + expect(CollectionResource.first.log_type).to eq("sales") + expect(CollectionResource.first.resource_type).to be_nil + expect(CollectionResource.first.mandatory).to be_falsey + expect(CollectionResource.first.released_to_user).to be_nil + expect(CollectionResource.first.display_name).to eq("sales some updated file (2025 to 2026)") + expect(CollectionResource.first.short_display_name).to eq("some updated file") + expect(page).to have_content("The sales 2025 to 2026 some updated file has been updated.") + end + + it "validates file is attached" do + visit(collection_resource_edit_path(collection_resource)) + + fill_in("collection_resource[short_display_name]", with: "some file") + click_button("Save changes") + expect(page).to have_content("Select which file to upload.") + end + end end diff --git a/spec/helpers/collection_resources_helper_spec.rb b/spec/helpers/collection_resources_helper_spec.rb index 859d14cd1..9fc77a4c9 100644 --- a/spec/helpers/collection_resources_helper_spec.rb +++ b/spec/helpers/collection_resources_helper_spec.rb @@ -94,9 +94,9 @@ RSpec.describe CollectionResourcesHelper do context "and next year resources were manually released" do before do - CollectionResource.create!(year: 2025, resource_type: "paper_form", display_name: "lettings log for tenants (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) - CollectionResource.create!(year: 2025, resource_type: "bulk_upload_template", display_name: "bulk upload template (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) - CollectionResource.create!(year: 2025, resource_type: "bulk_upload_specification", display_name: "sales log for tenants (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "paper_form", display_name: "lettings log for tenants (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "bulk_upload_template", display_name: "bulk upload template (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "bulk_upload_specification", display_name: "sales log for tenants (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) end it "reutrns current and next years" do @@ -199,9 +199,9 @@ RSpec.describe CollectionResourcesHelper do context "and the resources have been manually released" do before do - CollectionResource.create!(year: 2025, resource_type: "paper_form", display_name: "lettings log for tenants (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) - CollectionResource.create!(year: 2025, resource_type: "bulk_upload_template", display_name: "bulk upload template (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) - CollectionResource.create!(year: 2025, resource_type: "bulk_upload_specification", display_name: "sales log for tenants (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "paper_form", display_name: "lettings log for tenants (2025 to 2026)", download_filename: "file.pdf", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "bulk_upload_template", display_name: "bulk upload template (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) + create(:collection_resource, year: 2025, resource_type: "bulk_upload_specification", display_name: "sales log for tenants (2025 to 2026)", download_filename: "file.xlsx", mandatory: true, released_to_user: true) end it "returns false" do diff --git a/spec/requests/collection_resources_controller_spec.rb b/spec/requests/collection_resources_controller_spec.rb index 7c9bb0f05..a6d88f911 100644 --- a/spec/requests/collection_resources_controller_spec.rb +++ b/spec/requests/collection_resources_controller_spec.rb @@ -62,7 +62,7 @@ RSpec.describe CollectionResourcesController, type: :request do expect(page).to have_content("Sales 2025 to 2026") end - it "displays mandatory filed" do + it "displays mandatory files" do get collection_resources_path expect(page).to have_content("Paper form") @@ -70,6 +70,15 @@ RSpec.describe CollectionResourcesController, type: :request do expect(page).to have_content("Bulk upload specification") end + it "allows uploading new resources" do + get collection_resources_path + + expect(page).to have_link("Add new sales 2024 to 2025 resource", href: new_collection_resource_path(year: 2024, log_type: "sales")) + expect(page).to have_link("Add new lettings 2024 to 2025 resource", href: new_collection_resource_path(year: 2024, log_type: "lettings")) + expect(page).to have_link("Add new sales 2025 to 2026 resource", href: new_collection_resource_path(year: 2025, log_type: "sales")) + expect(page).to have_link("Add new lettings 2025 to 2026 resource", href: new_collection_resource_path(year: 2025, log_type: "lettings")) + end + context "when files are on S3" do before do allow(storage_service).to receive(:file_exists?).and_return(true) @@ -113,6 +122,16 @@ RSpec.describe CollectionResourcesController, type: :request do expect(page).to have_content("The 2025 to 2026 collection resources are not yet available to users.") expect(page).to have_link("Release the 2025 to 2026 collection resources to users", href: confirm_mandatory_collection_resources_release_path(year: 2025)) end + + context "when there are additional resources" do + let!(:collection_resource) { create(:collection_resource, :additional, year: 2025, short_display_name: "additional resource", download_filename: "additional.pdf") } + + it "displays change links for additional resources" do + get collection_resources_path + + expect(page).to have_link("Change", href: collection_resource_edit_path(collection_resource)) + end + end end context "when files are not on S3" do @@ -147,6 +166,26 @@ RSpec.describe CollectionResourcesController, type: :request do expect(page).to have_content("Once you have uploaded all the required 2025 to 2026 collection resources, you will be able to release them to users.") end end + + context "when there are additional resources" do + let!(:collection_resource) { create(:collection_resource, :additional, year: 2025, short_display_name: "additional resource", download_filename: "additional.pdf") } + + before do + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(CollectionResourcesHelper).to receive(:editable_collection_resource_years).and_return([2025]) + # rubocop:enable RSpec/AnyInstance + create(:collection_resource, :additional, year: 2026, short_display_name: "additional resource 2") + end + + it "displays additional resources for editable years" do + get collection_resources_path + + expect(page).to have_content("additional resource") + expect(page).not_to have_content("additional resource 2") + expect(page).to have_link("additional.pdf", href: collection_resource_download_path(collection_resource)) + expect(page).to have_link("Delete") + end + end end end @@ -473,4 +512,305 @@ RSpec.describe CollectionResourcesController, type: :request do end end end + + describe "GET #new_collection_resource" do + context "when user is not signed in" do + it "redirects to the sign in page" do + get new_collection_resource_path(year: 2025, log_type: "sales") + 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 new_collection_resource_path(year: 2025, log_type: "sales") + 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 new_collection_resource_path(year: 2025, log_type: "sales") + 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 + # rubocop:disable RSpec/AnyInstance + allow_any_instance_of(CollectionResourcesHelper).to receive(:editable_collection_resource_years).and_return([2025, 2026]) + # rubocop:enable RSpec/AnyInstance + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + end + + it "displays new collection resource page content" do + get new_collection_resource_path(year: 2025, log_type: "sales") + + expect(page).to have_content("Sales 2025 to 2026") + expect(page).to have_content("Add a new collection resource") + expect(page).to have_content("Upload file") + expect(page).to have_button("Add resource") + expect(page).to have_link("Back", href: collection_resources_path) + expect(page).to have_link("Cancel", href: collection_resources_path) + end + end + end + + describe "POST #collection_resources" do + let(:some_file) { File.open(file_fixture("blank_bulk_upload_sales.csv")) } + let(:params) { { collection_resource: { year: 2025, log_type: "sales", file: some_file, display_name: "some file" } } } + + context "when user is not signed in" do + it "redirects to the sign in page" do + post collection_resources_path, params: params + 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 + post collection_resources_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 + post collection_resources_path, params: params + expect(response).to have_http_status(:not_found) + end + end + end + + describe "GET #download_additional_collection_resource" do + let(:collection_resource) { create(:collection_resource, :additional, year: 2025, short_display_name: "additional resource") } + + 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 + end + + context "when the user is not signed in" do + context "when the file exists on S3" do + before do + allow(storage_service).to receive(:get_file).and_return("file") + get collection_resource_download_path(collection_resource) + end + + it "downloads the file" do + expect(response.body).to eq("file") + end + end + 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 + sign_in user + allow(storage_service).to receive(:get_file).and_return("file") + get collection_resource_download_path(collection_resource) + 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 + sign_in user + allow(storage_service).to receive(:get_file).and_return(nil) + get collection_resource_download_path(collection_resource) + end + + it "returns page not found" do + expect(response).to have_http_status(:not_found) + end + end + + context "when resource id is invalid" do + before do + sign_in user + allow(storage_service).to receive(:get_file).and_return(nil) + get collection_resource_download_path(collection_resource_id: "invalid") + 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 + let(:collection_resource) { create(:collection_resource, :additional, year: 2026, short_display_name: "additional resource") } + + before do + sign_in user + get collection_resource_download_path(collection_resource) + 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(:collection_resource) { create(:collection_resource, :additional, year: 2026, short_display_name: "additional resource") } + 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(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in user + allow(storage_service).to receive(:get_file).and_return("file") + get collection_resource_download_path(collection_resource) + 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_additional_collection_resource" do + let(:collection_resource) { create(:collection_resource, :additional, year: 2025, log_type: "sales", short_display_name: "additional resource", download_filename: "additional.pdf") } + + context "when user is not signed in" do + it "redirects to the sign in page" do + get collection_resource_edit_path(collection_resource) + 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_resource_edit_path(collection_resource) + 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_resource_edit_path(collection_resource) + 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 + + context "and the file exists on S3" do + before do + allow(storage_service).to receive(:file_exists?).and_return(true) + end + + it "displays update collection resources page content" do + get collection_resource_edit_path(collection_resource) + + expect(page).to have_content("Sales 2025 to 2026") + expect(page).to have_content("Change the additional resource") + 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 + end + + describe "PATCH #update_additional_collection_resource" do + let(:some_file) { File.open(file_fixture("blank_bulk_upload_sales.csv")) } + let(:params) { { collection_resource: { short_display_name: "short name", file: some_file } } } + let(:collection_resource_service) { instance_double(CollectionResourcesService) } + let(:collection_resource) { create(:collection_resource, :additional, year: 2025, log_type: "sales", short_display_name: "additional resource", download_filename: "additional.pdf") } + + 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 collection_resource_update_path(collection_resource), params: params + 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 collection_resource_update_path(collection_resource), 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 collection_resource_update_path(collection_resource), 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 db884d09f..b3846809c 100644 --- a/spec/requests/start_controller_spec.rb +++ b/spec/requests/start_controller_spec.rb @@ -324,6 +324,7 @@ RSpec.describe StartController, type: :request do context "and 2023 collection window is open for editing" do before do + create(:collection_resource, :additional, year: 2023, log_type: "sales", display_name: "sales additional resource (2023 to 2024)") allow(Time).to receive(:now).and_return(Time.zone.local(2024, 4, 1)) end @@ -337,6 +338,7 @@ RSpec.describe StartController, type: :request do expect(page).to have_content("Sales 23/24") expect(page).to have_content("Sales 2024 to 2025") expect(page).to have_content("Sales 2023 to 2024") + expect(page).to have_content("Download the sales additional resource (2023 to 2024)") end end diff --git a/spec/services/collection_resources_service_spec.rb b/spec/services/collection_resources_service_spec.rb index 3786b70c9..602e36c38 100644 --- a/spec/services/collection_resources_service_spec.rb +++ b/spec/services/collection_resources_service_spec.rb @@ -12,7 +12,7 @@ describe CollectionResourcesService do 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) + expect(storage_service).to receive(:write_file).with("2025_26_lettings_paper_form.pdf", some_file, content_type: "application/pdf") service.upload_collection_resource("2025_26_lettings_paper_form.pdf", some_file) end end