diff --git a/app/controllers/support/cases/document_uploads_controller.rb b/app/controllers/support/cases/document_uploads_controller.rb new file mode 100644 index 000000000..cec33d605 --- /dev/null +++ b/app/controllers/support/cases/document_uploads_controller.rb @@ -0,0 +1,56 @@ +module Support + class Cases::DocumentUploadsController < Cases::ApplicationController + before_action :set_current_case + before_action { @back_url = support_case_path(@current_case, anchor: "tasklist") } + before_action { @uploaded_files = current_case.upload_documents } + def edit + @document_uploader = current_case.document_uploader + @uploaded_files = current_case.upload_documents + end + + def create + @document_uploader = current_case.document_uploader(document_uploader_params) + if @document_uploader.valid? + @document_uploader.save! + @current_case.update!(case_document_uploader_params) + redirect_to @back_url + else + render :edit + end + end + + def destroy + @uploaded_document = Support::CaseUploadDocument.find(params[:document_id]) + @support_document = Support::Document.find(@uploaded_document.attachable_id) + @back_url = edit_support_case_document_uploads_path + return unless params[:confirm] + + @uploaded_document.destroy! + @support_document.destroy! + + if @uploaded_files.empty? + reset_uploaded_documents + end + redirect_to edit_support_case_document_uploads_path, + notice: I18n.t("support.cases.upload_documents.flash.destroyed", name: @uploaded_document.file_name) + end + + private + + def set_current_case + @current_case = Support::Case.find(params[:case_id]) + end + + def document_uploader_params + params.fetch(:document_uploader, {}).permit(:has_uploaded_documents, files: []) + end + + def case_document_uploader_params + params.require(:document_uploader).permit(:has_uploaded_documents) + end + + def reset_uploaded_documents + @current_case.update!(has_uploaded_documents: nil) + end + end +end diff --git a/app/controllers/support/document_downloads_controller.rb b/app/controllers/support/document_downloads_controller.rb index 4e7b5a17f..e20d1eb9f 100644 --- a/app/controllers/support/document_downloads_controller.rb +++ b/app/controllers/support/document_downloads_controller.rb @@ -8,6 +8,7 @@ class DocumentDownloadsController < Cases::ApplicationController "Support::CaseAttachment", "EnergyBill", "Support::EmailTemplateAttachment", + "Support::CaseUploadDocument", ].freeze def show diff --git a/app/models/support/case.rb b/app/models/support/case.rb index cfb02886b..a01a7e309 100644 --- a/app/models/support/case.rb +++ b/app/models/support/case.rb @@ -23,6 +23,7 @@ class Case < ApplicationRecord include FileUploadable include Surveyable include Notifiable + include DocumentUploadable belongs_to :category, class_name: "Support::Category", optional: true belongs_to :query, class_name: "Support::Query", optional: true @@ -63,6 +64,8 @@ class Case < ApplicationRecord accepts_nested_attributes_for :hub_transition, allow_destroy: true, reject_if: :all_blank + has_many :upload_documents, class_name: "Support::CaseUploadDocument", foreign_key: :support_case_id + # Support level # # L1 - Advice and guidance only diff --git a/app/models/support/case/document_uploadable.rb b/app/models/support/case/document_uploadable.rb new file mode 100644 index 000000000..9a21fbfa6 --- /dev/null +++ b/app/models/support/case/document_uploadable.rb @@ -0,0 +1,20 @@ +module Support::Case::DocumentUploadable + extend ActiveSupport::Concern + + def document_uploader(params = {}) + Support::Case::DocumentUploader.new(support_case: self, **params) + end + + def upload_document_files(files:) + return if files.blank? + + files.each do |file| + upload_documents.create!( + attachable: Support::Document.create!(case: self, file_type: file.content_type, file:), + file_type: file.content_type, + file_name: file.original_filename, + file_size: file.size, + ) + end + end +end diff --git a/app/models/support/case/document_uploader.rb b/app/models/support/case/document_uploader.rb new file mode 100644 index 000000000..249d7a469 --- /dev/null +++ b/app/models/support/case/document_uploader.rb @@ -0,0 +1,25 @@ +class Support::Case::DocumentUploader + include ActiveModel::Model + include ActiveModel::Attributes + include ActiveModel::Validations + + attribute :files + attribute :upload_document_files + attribute :support_case + attribute :has_uploaded_documents + + validates :files, presence: true, if: -> { support_case.has_uploaded_documents.nil? } + validate :files_safe, if: -> { files.present? } + validates :has_uploaded_documents, presence: true + + def save! + support_case.upload_document_files(files:) + end + +private + + def files_safe + results = files.map { |file| Support::VirusScanner.uploaded_file_safe?(file) } + errors.add(:files, I18n.t("support.case.label.case_files.errors.unsafe")) unless results.all? + end +end diff --git a/app/models/support/case_upload_document.rb b/app/models/support/case_upload_document.rb new file mode 100644 index 000000000..eb9fc0cce --- /dev/null +++ b/app/models/support/case_upload_document.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Support + class CaseUploadDocument < ApplicationRecord + belongs_to :case, class_name: "Support::Case", foreign_key: :support_case_id + belongs_to :attachable, polymorphic: true, optional: true + + delegate :file, :file_type, to: :attachable + end +end diff --git a/app/views/support/cases/document_uploads/_form.html.erb b/app/views/support/cases/document_uploads/_form.html.erb new file mode 100644 index 000000000..eb61e631b --- /dev/null +++ b/app/views/support/cases/document_uploads/_form.html.erb @@ -0,0 +1,38 @@ +<%= form_with model: @document_uploader, scope: :document_uploader, url: support_case_document_uploads_path, + html: { "data-controller" => "case-files" } do |form| %> + +
<%= I18n.t("support.cases.upload_documents.hint") %>
+ +<%= render "form" %> \ No newline at end of file diff --git a/app/views/support/cases/document_uploads/index.html.erb b/app/views/support/cases/document_uploads/index.html.erb new file mode 100644 index 000000000..2a832e98d --- /dev/null +++ b/app/views/support/cases/document_uploads/index.html.erb @@ -0,0 +1,5 @@ +<%= render "framework_requests/form", + route: "document_uploads", + verb: :post, + path: document_uploads_framework_requests_path, + hide_submit_button: true %> diff --git a/app/views/support/cases/show/_tasklist.html.erb b/app/views/support/cases/show/_tasklist.html.erb index c79ea52e1..352157ac9 100644 --- a/app/views/support/cases/show/_tasklist.html.erb +++ b/app/views/support/cases/show/_tasklist.html.erb @@ -10,7 +10,16 @@ <%= govuk_task_list(id_prefix: "complete-evaluation") do |task_list| task_list.with_item(title: I18n.t("support.case.label.tasklist.item.add_evaluators"), href: '#', status: govuk_tag(text: I18n.t("support.case.label.tasklist.status.to_do"))) task_list.with_item(title: I18n.t("support.case.label.tasklist.item.set_due_date"), href: edit_support_case_evaluation_due_dates_path(@current_case), status: @current_case.evaluation_due_date ? govuk_tag(text: I18n.t("support.case.label.tasklist.status.complete"), colour: "green") : govuk_tag(text: I18n.t("support.case.label.tasklist.status.to_do"))) - task_list.with_item(title: I18n.t("support.case.label.tasklist.item.upload_documents"), href: '#', status: govuk_tag(text: I18n.t("support.case.label.tasklist.status.to_do"))) + + if @current_case.has_uploaded_documents == true + document_upload_Status = govuk_tag(text: I18n.t("support.case.label.tasklist.status.complete"), colour: "green") + elsif @current_case.has_uploaded_documents == false + document_upload_Status = govuk_tag(text: I18n.t("support.case.label.tasklist.status.in_progress")) + else + document_upload_Status = govuk_tag(text: I18n.t("support.case.label.tasklist.status.to_do")) + end + + task_list.with_item(title: I18n.t("support.case.label.tasklist.item.upload_documents"), href: edit_support_case_document_uploads_path(@current_case), status: document_upload_Status) task_list.with_item(title: I18n.t("support.case.label.tasklist.item.email_evaluators")) do | item | item.with_status(text: I18n.t("support.case.label.tasklist.status.cannot_start"), cannot_start_yet: true) end diff --git a/config/locales/en.yml b/config/locales/en.yml index fb06d85c5..f89f3fa81 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1780,6 +1780,21 @@ en: hint: For example, 27 3 2025 submit: Continue cancel: Cancel + upload_documents: + header: Upload documents + hint: Upload documents to share with each evaluator. + choose_file_title: Upload a file + file_uploaded: File uploaded + file_upload_confirmation: Have you uploaded all documents? + radio_buttons: + yes: "Yes, I have uploaded all documents" + no: "No" + button: + choose_file: Choose file + flash: + destroyed: "%{name} successfully removed" + delete_confirmation: Are you sure you want to delete %{name}? + is_document_uploaded: Please select uploaded option emails: attachments: attach_files: Attach files diff --git a/config/locales/validation/support/en.yml b/config/locales/validation/support/en.yml index 4105b6ac2..b73d548f1 100644 --- a/config/locales/validation/support/en.yml +++ b/config/locales/validation/support/en.yml @@ -59,6 +59,14 @@ en: attributes: files: blank: Select files to upload + support/case/document_uploader: + attributes: + files: + blank: Select files to upload + has_uploaded_documents: + blank: Please confirm that you uploaded all documents + + forms: rules: diff --git a/config/routes.rb b/config/routes.rb index 091660488..67a05aa1b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -236,6 +236,7 @@ resources :additional_contacts resources :evaluators, except: %i[show] resource :evaluation_due_dates, only: %i[edit update] + resource :document_uploads, except: %i[show] resource :email, only: %i[create] do scope module: :emails do resources :content, only: %i[show], param: :template diff --git a/db/migrate/20241203125734_add_support_case_upload_documents.rb b/db/migrate/20241203125734_add_support_case_upload_documents.rb new file mode 100644 index 000000000..855239143 --- /dev/null +++ b/db/migrate/20241203125734_add_support_case_upload_documents.rb @@ -0,0 +1,13 @@ +class AddSupportCaseUploadDocuments < ActiveRecord::Migration[7.2] + def change + create_table "support_case_upload_documents", id: :uuid do |t| + t.references "support_case", type: :uuid + t.string "file_type" + t.string "file_name" + t.bigint "file_size" + t.uuid "attachable_id" + t.string "attachable_type" + t.timestamps + end + end +end diff --git a/db/migrate/20241209104836_add_has_uploaded_documents_to_support_cases.rb b/db/migrate/20241209104836_add_has_uploaded_documents_to_support_cases.rb new file mode 100644 index 000000000..475190508 --- /dev/null +++ b/db/migrate/20241209104836_add_has_uploaded_documents_to_support_cases.rb @@ -0,0 +1,5 @@ +class AddHasUploadedDocumentsToSupportCases < ActiveRecord::Migration[7.2] + def change + add_column :support_cases, :has_uploaded_documents, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 09646b58e..105518eea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_11_26_122758) do +ActiveRecord::Schema[7.2].define(version: 2024_12_09_104836) do create_sequence "evaluation_refs" create_sequence "framework_refs" @@ -573,6 +573,18 @@ t.index ["support_organisation_id"], name: "index_support_case_organisations_on_support_organisation_id" end + create_table "support_case_upload_documents", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.uuid "support_case_id" + t.string "file_type" + t.string "file_name" + t.bigint "file_size" + t.uuid "attachable_id" + t.string "attachable_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["support_case_id"], name: "index_support_case_upload_documents_on_support_case_id" + end + create_table "support_cases", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "ref" t.uuid "category_id" @@ -624,6 +636,7 @@ t.string "other_school_urns", default: [], array: true t.boolean "is_evaluator", default: false t.date "evaluation_due_date" + t.boolean "has_uploaded_documents" t.index ["category_id"], name: "index_support_cases_on_category_id" t.index ["existing_contract_id"], name: "index_support_cases_on_existing_contract_id" t.index ["new_contract_id"], name: "index_support_cases_on_new_contract_id" diff --git a/spec/features/support/agent_can_upload_evaluation_documents_spec.rb b/spec/features/support/agent_can_upload_evaluation_documents_spec.rb new file mode 100644 index 000000000..f4038c15f --- /dev/null +++ b/spec/features/support/agent_can_upload_evaluation_documents_spec.rb @@ -0,0 +1,80 @@ +require "rails_helper" + +RSpec.feature "Agent can upload evaluation documents", :js, :with_csrf_protection do + include_context "with an agent" + + let(:support_case) { create(:support_case) } + let(:file_1) { fixture_file_upload(Rails.root.join("spec/fixtures/support/text-file.txt"), "text/plain") } + let(:file_2) { fixture_file_upload(Rails.root.join("spec/fixtures/support/another-text-file.txt"), "text/plain") } + let(:document_uploader) { support_case.document_uploader(files: [file_1, file_2]) } + + specify "when no files are selected" do + visit edit_support_case_document_uploads_path(case_id: support_case) + + expect(page).to have_text("Upload documents") + expect(page).to have_text("Upload a file") + expect(page).to have_text("Have you uploaded all documents?") + + click_button "Continue" + + expect(page).to have_text("There is a problem") + expect(page).to have_text("Select files to upload") + expect(page).to have_text("Please confirm that you uploaded all documents") + end + + specify "when files are uploaded and confirmation choosen as No (In progress)" do + support_case.update!(has_uploaded_documents: false) + + expect { document_uploader.save! }.to change { support_case.upload_documents.count }.from(0).to(2) + expect(support_case.upload_documents.pluck(:file_name)).to contain_exactly("text-file.txt", "another-text-file.txt") + expect(support_case.upload_documents.map { |a| a.file.attached? }.all?).to eq(true) + + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-3-status")).to have_text("In progress") + end + + specify "when files are uploaded and confirmation choosen as Yes (Complete)" do + support_case.update!(has_uploaded_documents: true) + + expect { document_uploader.save! }.to change { support_case.upload_documents.count }.from(0).to(2) + expect(support_case.upload_documents.pluck(:file_name)).to contain_exactly("text-file.txt", "another-text-file.txt") + expect(support_case.upload_documents.map { |a| a.file.attached? }.all?).to eq(true) + + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-3-status")).to have_text("Complete") + end + + specify "viewing uploaded files" do + support_case.update!(has_uploaded_documents: true) + document_uploader.save! + + visit edit_support_case_document_uploads_path(case_id: support_case) + + expect(page).to have_content("text-file.txt") + expect(page).to have_content("another-text-file.txt") + + find_all(".case-files__file-remove a")[0].click + + expect(find(".govuk-button--warning")).to have_content("Delete") + + expect(page).to have_content("text-file.txt") + end + + specify "when all files are deleted" do + support_case.reload + + support_case.upload_documents(&:destroy!) + + support_case.reload + + support_case.update!(has_uploaded_documents: nil) + + expect(support_case.upload_documents.count).to eq(0) + + visit support_case_path(support_case, anchor: "tasklist") + + expect(find("#complete-evaluation-3-status", wait: 10)).to have_text("To do") + end +end diff --git a/spec/models/support/case_spec.rb b/spec/models/support/case_spec.rb index ac1912a4d..58a209031 100644 --- a/spec/models/support/case_spec.rb +++ b/spec/models/support/case_spec.rb @@ -59,7 +59,7 @@ describe "#to_csv" do it "includes headers" do expect(described_class.to_csv).to eql( - "id,ref,category_id,request_text,support_level,status,state,created_at,updated_at,agent_id,first_name,last_name,email,phone_number,source,organisation_id,existing_contract_id,new_contract_id,procurement_id,savings_status,savings_estimate_method,savings_actual_method,savings_estimate,savings_actual,action_required,organisation_type,value,closure_reason,extension_number,other_category,other_query,procurement_amount,confidence_level,special_requirements,query_id,exit_survey_sent,detected_category_id,creation_source,user_selected_category,created_by_id,procurement_stage_id,initial_request_text,with_school,next_key_date,next_key_date_description,discovery_method,discovery_method_other_text,project,other_school_urns,is_evaluator,evaluation_due_date\n", + "id,ref,category_id,request_text,support_level,status,state,created_at,updated_at,agent_id,first_name,last_name,email,phone_number,source,organisation_id,existing_contract_id,new_contract_id,procurement_id,savings_status,savings_estimate_method,savings_actual_method,savings_estimate,savings_actual,action_required,organisation_type,value,closure_reason,extension_number,other_category,other_query,procurement_amount,confidence_level,special_requirements,query_id,exit_survey_sent,detected_category_id,creation_source,user_selected_category,created_by_id,procurement_stage_id,initial_request_text,with_school,next_key_date,next_key_date_description,discovery_method,discovery_method_other_text,project,other_school_urns,is_evaluator,evaluation_due_date,has_uploaded_documents\n", ) end end