diff --git a/app/controllers/forms/translations_controller.rb b/app/controllers/forms/translations_controller.rb new file mode 100644 index 0000000000..6f9baf5cee --- /dev/null +++ b/app/controllers/forms/translations_controller.rb @@ -0,0 +1,7 @@ +module Forms + class TranslationsController < WebController + after_action :verify_authorized + + def new; end + end +end diff --git a/app/models/form.rb b/app/models/form.rb index fba8dde22f..a99855b779 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -43,7 +43,7 @@ class Form < ApplicationRecord after_create :set_external_id after_update :update_draft_form_document - ATTRIBUTES_NOT_IN_FORM_DOCUMENT = %i[state external_id pages question_section_completed declaration_section_completed share_preview_completed].freeze + ATTRIBUTES_NOT_IN_FORM_DOCUMENT = %i[state external_id pages question_section_completed declaration_section_completed share_preview_completed welsh_completed].freeze def save_draft! save! @@ -184,6 +184,8 @@ def update_draft_form_document end def task_status_service + # TODO: refactor this in favour of dependency injection + # it can also lead to use of `allow_any_instance_of` in testing @task_status_service ||= TaskStatusService.new(form: self) end @@ -196,6 +198,8 @@ def group_form end def email_task_status_service + # TODO: refactor this in favour of dependency injection + # it can also lead to use of `allow_any_instance_of` in testing @email_task_status_service ||= EmailTaskStatusService.new(form: self) end diff --git a/app/services/form_task_list_service.rb b/app/services/form_task_list_service.rb index 5b840a5874..209704bf58 100644 --- a/app/services/form_task_list_service.rb +++ b/app/services/form_task_list_service.rb @@ -17,14 +17,24 @@ def initialize(form:, current_user:) end def all_sections - [ - create_form_section, + sections = [ + create_form_section(section_number: 1), payment_link_subsection, - email_address_section, + email_address_section(section_number: 2), receive_csv_subsection, - privacy_and_contact_details_section, - make_form_live_section, + privacy_and_contact_details_section(section_number: 3), ] + + last_sections = [make_form_live_section(section_number: 4)] + + if FeatureService.new(group: @form.group).enabled?(:welsh) + last_sections = [ + translations_section(section_number: 4), + make_form_live_section(section_number: 5), + ] + end + + sections + last_sections end private @@ -36,11 +46,11 @@ def create_or_edit "create" end - def create_form_section + def create_form_section(section_number:) { title: I18n.t("forms.task_list_#{create_or_edit}.create_form_section.title"), rows: create_form_section_tasks, - section_number: 1, + section_number:, subsection: false, } end @@ -74,10 +84,10 @@ def payment_link_subsection_tasks ] end - def email_address_section + def email_address_section(section_number:) { title: I18n.t("forms.task_list_#{create_or_edit}.email_address_section.title"), - section_number: 2, + section_number:, subsection: false, rows: email_address_section_tasks, } @@ -104,11 +114,11 @@ def receive_csv_subsection_tasks ] end - def privacy_and_contact_details_section + def privacy_and_contact_details_section(section_number:) { title: I18n.t("forms.task_list_#{create_or_edit}.privacy_and_contact_details_section.title"), rows: privacy_and_contact_details_section_tasks, - section_number: 3, + section_number:, subsection: false, } end @@ -120,10 +130,25 @@ def privacy_and_contact_details_section_tasks ] end - def make_form_live_section + def translations_section(section_number:) + { + title: I18n.t("forms.task_list_#{create_or_edit}.translations_section.title"), + rows: translations_section_task, + section_number:, + subsection: false, + } + end + + def translations_section_task + [ + { task_name: I18n.t("forms.task_list_#{create_or_edit}.translations_section.add_welsh"), path: translations_path(@form.id), status: @task_statuses[:welsh_language_status] }, + ] + end + + def make_form_live_section(section_number:) section = { title: live_title_name, - section_number: 4, + section_number:, subsection: false, } diff --git a/app/services/task_status_service.rb b/app/services/task_status_service.rb index e410e39c43..cd343bbec3 100644 --- a/app/services/task_status_service.rb +++ b/app/services/task_status_service.rb @@ -16,7 +16,7 @@ def incomplete_tasks end def task_statuses - { + statuses = { name_status:, pages_status:, declaration_status:, @@ -28,6 +28,12 @@ def task_statuses share_preview_status:, make_live_status:, } + + if FeatureService.new(group: @form.group).enabled?(:welsh) + statuses.merge!({ welsh_language_status: }) + end + + statuses end private @@ -74,6 +80,13 @@ def support_contact_details_status :not_started end + def welsh_language_status + return :completed if @form.welsh_completed? + return :in_progress if @form.available_languages.include?("cy") && !@form.welsh_completed? + + :optional + end + def receive_csv_status return :completed if @form.email_with_csv? diff --git a/config/locales/en.yml b/config/locales/en.yml index 8bcc137cda..14819ec9a3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -407,6 +407,9 @@ en: receive_csv_subsection: receive_csv: Get completed forms as CSV files title: Optional task + translations_section: + add_welsh: Add Welsh language version of your form + title: Add translations to your form (optional) task_list_edit: create_form_section: declaration: Edit the declaration for people to agree to @@ -433,6 +436,9 @@ en: receive_csv_subsection: receive_csv: Get completed forms as CSV files title: Optional task + translations_section: + add_welsh: Edit Welsh language version of your form + title: Add translations to your form (optional) group_forms: edit: body_html: "
We’ll send an email to members of the current group to let them know the form has moved and they may no longer have access to it.
\n" diff --git a/config/routes.rb b/config/routes.rb index aa8e217f3b..b3a9180aef 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,6 +64,8 @@ post "/receive-csv" => "forms/receive_csv#create", as: :receive_csv_create get "/share-preview" => "forms/share_preview#new", as: :share_preview post "/share-preview" => "forms/share_preview#create", as: :share_preview_create + get "/translations" => "forms/translations#new", as: :translations + post "/translations" => "forms/translations#create", as: :translations_create scope "/pages" do get "/" => "pages#index", as: :form_pages diff --git a/db/migrate/20251020132955_add_welsh_completed_to_forms.rb b/db/migrate/20251020132955_add_welsh_completed_to_forms.rb new file mode 100644 index 0000000000..a24f06d5df --- /dev/null +++ b/db/migrate/20251020132955_add_welsh_completed_to_forms.rb @@ -0,0 +1,5 @@ +class AddWelshCompletedToForms < ActiveRecord::Migration[8.0] + def change + add_column :forms, :welsh_completed, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 262f64538e..dcd55ff847 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[8.0].define(version: 2025_10_07_152124) do +ActiveRecord::Schema[8.0].define(version: 2025_10_20_132955) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -140,6 +140,7 @@ t.string "s3_bucket_region" t.string "language", default: "en", null: false t.text "available_languages", default: ["en"], null: false, array: true + t.boolean "welsh_completed", default: false t.index ["external_id"], name: "index_forms_on_external_id", unique: true end diff --git a/spec/factories/models/forms.rb b/spec/factories/models/forms.rb index 32dfe71748..cb4b571c6d 100644 --- a/spec/factories/models/forms.rb +++ b/spec/factories/models/forms.rb @@ -19,6 +19,23 @@ payment_url { nil } external_id { nil } + trait :with_group do + transient do + group { nil } + end + + after(:build) do |form, evaluator| + g = evaluator.group || FactoryBot.create(:group) + form.instance_variable_set(:@associated_group, g) + form.define_singleton_method(:group) { g } + end + + after(:create) do |form, _evaluator| + g = form.instance_variable_get(:@associated_group) + GroupForm.create!(form_id: form.id, group_id: g.id) unless GroupForm.exists?(form_id: form.id, group_id: g.id) + end + end + trait :new_form do submission_email { "" } privacy_policy_url { "" } diff --git a/spec/factories/models/groups.rb b/spec/factories/models/groups.rb index 5af98487bd..20eb8fc071 100644 --- a/spec/factories/models/groups.rb +++ b/spec/factories/models/groups.rb @@ -4,9 +4,14 @@ organisation { association :organisation, id: 1, slug: "test-org" } creator { association :user, organisation: } status { :trial } + welsh_enabled { false } trait :org_has_org_admin do organisation { association :organisation, :with_org_admin, id: 1, slug: "test-org" } end + + trait :with_welsh_enabled do + welsh_enabled { true } + end end end diff --git a/spec/models/form_document/content_spec.rb b/spec/models/form_document/content_spec.rb index b51a8bc1f9..e31aaeb9d7 100644 --- a/spec/models/form_document/content_spec.rb +++ b/spec/models/form_document/content_spec.rb @@ -28,7 +28,7 @@ end it "has all form attributes the original form has" do - expected_attributes = form.attributes.except(*%w[id state external_id pages question_section_completed declaration_section_completed share_preview_completed]) + expected_attributes = form.attributes.except(*%w[id state external_id pages question_section_completed declaration_section_completed share_preview_completed welsh_completed]) expect(form_document_content).to have_attributes(expected_attributes) end diff --git a/spec/models/form_spec.rb b/spec/models/form_spec.rb index 2aa1d3b058..8b104966b5 100644 --- a/spec/models/form_spec.rb +++ b/spec/models/form_spec.rb @@ -10,7 +10,7 @@ end it "has a ready for live trait" do - form = build :form, :ready_for_live + form = build :form, :ready_for_live, :with_group expect(form.ready_for_live).to be true expect(form.incomplete_tasks).to be_empty expect(form.task_statuses).to include( @@ -619,7 +619,8 @@ end describe "#all_task_statuses" do - let(:completed_form) { build :form, :live } + let(:group) { create :group, :with_welsh_enabled } + let(:completed_form) { build :form, :live, :with_group, group: } it "returns a hash with each of the task statuses" do expected_hash = { @@ -633,6 +634,7 @@ payment_link_status: :optional, receive_csv_status: :optional, support_contact_details_status: :completed, + welsh_language_status: :optional, share_preview_status: :completed, make_live_status: :completed, } @@ -841,7 +843,7 @@ let(:form) { create :form, :ready_for_live } it "includes all attributes for the form" do - form_attributes = described_class.attribute_names - %w[id state external_id pages question_section_completed declaration_section_completed share_preview_completed] + form_attributes = described_class.attribute_names - %w[id state external_id pages question_section_completed declaration_section_completed share_preview_completed welsh_completed] expect(form.as_form_document).to match a_hash_including(*form_attributes) end diff --git a/spec/services/form_task_list_service_spec.rb b/spec/services/form_task_list_service_spec.rb index d612bb30ea..401e1d858c 100644 --- a/spec/services/form_task_list_service_spec.rb +++ b/spec/services/form_task_list_service_spec.rb @@ -88,9 +88,34 @@ expect(all_sections).to be_an_instance_of(Array) end - it "returns 5 sections" do - expected_sections = [{ title: "Task 1" }, { title: "Task 2" }, { title: "Task 3" }, { title: "Task 4" }, { title: "Task 5" }, { title: "Task 6" }] - expect(all_sections.count).to eq expected_sections.count + context "when welsh is not enabled" do + it "returns 6 sections" do + expect(all_sections.count).to eq 6 + end + + it "does not include translations section" do + section_titles = all_sections.map { |section| section[:title] } + expect(section_titles).not_to include(I18n.t("forms.task_list_create.translations_section.title")) + end + end + + context "when welsh is enabled" do + let(:group) { create(:group, :with_welsh_enabled, name: "Group 1", organisation:, status: group_status) } + + it "returns 7 sections" do + expect(all_sections.count).to eq 7 + end + + it "includes translations section" do + section_titles = all_sections.map { |section| section[:title] } + expect(section_titles).to include(I18n.t("forms.task_list_create.translations_section.title")) + end + + it "has translations section before make form live section" do + translations_section_index = all_sections.index { |section| section[:title] == I18n.t("forms.task_list_create.translations_section.title") } + make_live_section_index = all_sections.index { |section| section[:title] == I18n.t("forms.task_list_create.make_form_live_section.title") } + expect(translations_section_index).to be < make_live_section_index + end end describe "create form section tasks" do diff --git a/spec/services/task_status_service_spec.rb b/spec/services/task_status_service_spec.rb index 01b4ca5d71..099a315ff2 100644 --- a/spec/services/task_status_service_spec.rb +++ b/spec/services/task_status_service_spec.rb @@ -1,6 +1,7 @@ require "rails_helper" describe TaskStatusService do + let(:group) { create(:group, :with_welsh_enabled) } let(:task_status_service) do described_class.new(form:) end @@ -9,7 +10,7 @@ describe "statuses" do describe "name status" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:name_status]).to eq :completed @@ -18,7 +19,7 @@ describe "pages status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:pages_status]).to eq :not_started @@ -26,14 +27,14 @@ end context "with a form which has pages" do - let(:form) { build(:form, :new_form, :with_pages, question_section_completed: false) } + let(:form) { build(:form, :new_form, :with_pages, :with_group, question_section_completed: false, group:) } it "returns the in progress status" do expect(task_status_service.task_statuses[:pages_status]).to eq :in_progress end context "and questions marked completed" do - let(:form) { build(:form, :new_form, :with_pages, question_section_completed: true) } + let(:form) { build(:form, :new_form, :with_pages, :with_group, question_section_completed: true, group:) } it "returns the completed status" do expect(task_status_service.task_statuses[:pages_status]).to eq :completed @@ -44,7 +45,7 @@ describe "declaration status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:declaration_status]).to eq :not_started @@ -52,7 +53,7 @@ end context "with a form which has no declaration content and is marked incomplete" do - let(:form) { build(:form, declaration_section_completed: false) } + let(:form) { build(:form, :with_group, declaration_section_completed: false, group:) } it "returns the not started status" do expect(task_status_service.task_statuses[:declaration_status]).to eq :not_started @@ -60,7 +61,7 @@ end context "with a form which has declaration content and is marked incomplete" do - let(:form) { build(:form, declaration_text: "I understand the implications", declaration_section_completed: false) } + let(:form) { build(:form, :with_group, declaration_text: "I understand the implications", declaration_section_completed: false, group:) } it "returns the in progress status" do expect(task_status_service.task_statuses[:declaration_status]).to eq :in_progress @@ -68,7 +69,7 @@ end context "with a form which has a declaration marked complete" do - let(:form) { build(:form, declaration_section_completed: true) } + let(:form) { build(:form, :with_group, declaration_section_completed: true, group:) } it "returns the completed status" do expect(task_status_service.task_statuses[:declaration_status]).to eq :completed @@ -78,7 +79,7 @@ describe "what happens next status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:what_happens_next_status]).to eq :not_started @@ -86,7 +87,7 @@ end context "with a form which has a what_happens_next_markdown" do - let(:form) { build(:form, :new_form, what_happens_next_markdown: "We usually respond to applications within 10 working days.") } + let(:form) { build(:form, :new_form, :with_group, what_happens_next_markdown: "We usually respond to applications within 10 working days.", group:) } it "returns the completed status" do expect(task_status_service.task_statuses[:what_happens_next_status]).to eq :completed @@ -96,7 +97,7 @@ describe "payment link status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:payment_link_status]).to eq :optional @@ -104,7 +105,7 @@ end context "with a form with a payment link" do - let(:form) { build(:form, :new_form, payment_url: Faker::Internet.url(host: "gov.uk")) } + let(:form) { build(:form, :new_form, :with_group, payment_url: Faker::Internet.url(host: "gov.uk"), group:) } it "returns the completed status" do expect(task_status_service.task_statuses[:payment_link_status]).to eq :completed @@ -114,7 +115,7 @@ describe "privacy policy status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:privacy_policy_status]).to eq :not_started @@ -122,7 +123,7 @@ end context "with a form which has a privacy policy section" do - let(:form) { build(:form, :new_form, privacy_policy_url: Faker::Internet.url(host: "gov.uk")) } + let(:form) { build(:form, :new_form, :with_group, privacy_policy_url: Faker::Internet.url(host: "gov.uk"), group:) } it "returns the in progress status" do expect(task_status_service.task_statuses[:privacy_policy_status]).to eq :completed @@ -132,7 +133,7 @@ describe "support contact details status status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:support_contact_details_status]).to eq :not_started @@ -140,7 +141,7 @@ end context "with a form which has contact details set" do - let(:form) { build(:form, :new_form, :with_support) } + let(:form) { build(:form, :new_form, :with_support, :with_group, group:) } it "returns the in progress status" do expect(task_status_service.task_statuses[:support_contact_details_status]).to eq :completed @@ -150,7 +151,7 @@ describe "receive_csv_status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns optional" do expect(task_status_service.task_statuses[:receive_csv_status]).to eq :optional @@ -158,7 +159,7 @@ end context "with submission_type set to 'email'" do - let(:form) { build(:form, :new_form, submission_type: "email") } + let(:form) { build(:form, :new_form, :with_group, submission_type: "email", group:) } it "returns optional" do expect(task_status_service.task_statuses[:receive_csv_status]).to eq :optional @@ -166,7 +167,7 @@ end context "with submission_type set to 'email_with_csv'" do - let(:form) { build(:form, :new_form, submission_type: "email_with_csv") } + let(:form) { build(:form, :new_form, :with_group, submission_type: "email_with_csv", group:) } it "returns completed" do expect(task_status_service.task_statuses[:receive_csv_status]).to eq :completed @@ -177,7 +178,7 @@ describe "share_preview_status" do context "with share_preview_completed set to false" do context "when the form does not have any pages" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns cannot_start" do expect(task_status_service.task_statuses[:share_preview_status]).to eq :cannot_start @@ -185,7 +186,7 @@ end context "when the form has pages" do - let(:form) { build(:form, :with_pages) } + let(:form) { build(:form, :with_pages, :with_group, group:) } it "returns not_started" do expect(task_status_service.task_statuses[:share_preview_status]).to eq :not_started @@ -195,7 +196,7 @@ context "with share_preview_completed set to true" do context "when the form does not have any pages" do - let(:form) { build(:form, :new_form, share_preview_completed: true) } + let(:form) { build(:form, :new_form, :with_group, share_preview_completed: true, group:) } it "returns cannot_start" do expect(task_status_service.task_statuses[:share_preview_status]).to eq :cannot_start @@ -203,7 +204,7 @@ end context "when the form has pages" do - let(:form) { build(:form, :with_pages, share_preview_completed: true) } + let(:form) { build(:form, :with_pages, :with_group, share_preview_completed: true, group:) } it "returns completed" do expect(task_status_service.task_statuses[:share_preview_status]).to eq :completed @@ -214,7 +215,7 @@ describe "make live status" do context "with a new form" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns the correct default value" do expect(task_status_service.task_statuses[:make_live_status]).to eq :cannot_start @@ -222,7 +223,7 @@ end context "with a form which is ready to go live" do - let(:form) { build(:form, :ready_for_live) } + let(:form) { build(:form, :ready_for_live, :with_group, group:) } it "returns the not started status" do expect(task_status_service.task_statuses[:make_live_status]).to eq :not_started @@ -230,7 +231,7 @@ end context "with a live form with a draft and all tasks complete" do - let(:form) { build(:form, :ready_for_live, state: :live_with_draft) } + let(:form) { build(:form, :ready_for_live, :with_group, state: :live_with_draft, group:) } it "returns the not started status" do expect(task_status_service.task_statuses[:make_live_status]).to eq :not_started @@ -238,7 +239,7 @@ end context "with an archived form with a draft and incomplete tasks" do - let(:form) { build(:form, state: :archived_with_draft) } + let(:form) { build(:form, :with_group, state: :archived_with_draft, group:) } it "returns the not started status" do expect(task_status_service.task_statuses[:make_live_status]).to eq :cannot_start @@ -246,7 +247,7 @@ end context "with an archived form with a draft and all tasks complete" do - let(:form) { build(:form, :ready_for_live, state: :archived_with_draft) } + let(:form) { build(:form, :ready_for_live, :with_group, state: :archived_with_draft, group:) } it "returns the not started status" do expect(task_status_service.task_statuses[:make_live_status]).to eq :not_started @@ -254,7 +255,7 @@ end context "with a live form" do - let(:form) { create(:form, :live) } + let(:form) { create(:form, :live, :with_group, group:) } it "returns the completed status" do expect(task_status_service.task_statuses[:make_live_status]).to eq :completed @@ -262,7 +263,7 @@ end context "with an archived form" do - let(:form) { build(:form, state: :archived) } + let(:form) { build(:form, :with_group, state: :archived, group:) } it "returns the completed status" do expect(task_status_service.task_statuses[:make_live_status]).to eq :not_started @@ -273,7 +274,7 @@ describe "#mandatory_tasks_completed" do context "when mandatory tasks have not been completed" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns false" do expect(task_status_service.mandatory_tasks_completed?).to be false @@ -281,7 +282,7 @@ end context "when mandatory tasks have been completed" do - let(:form) { build(:form, :ready_for_live) } + let(:form) { build(:form, :ready_for_live, :with_group, group:) } it "returns true" do expect(task_status_service.mandatory_tasks_completed?).to be true @@ -291,7 +292,7 @@ describe "#incomplete_tasks" do context "when mandatory tasks are complete" do - let(:form) { build(:form, :live) } + let(:form) { build(:form, :live, :with_group, group:) } it "returns no missing sections" do expect(task_status_service.incomplete_tasks).to be_empty @@ -299,7 +300,7 @@ end context "when a form is incomplete and should still be in draft state" do - let(:form) { build(:form, :new_form) } + let(:form) { build(:form, :new_form, :with_group, group:) } it "returns a set of keys related to missing fields" do expect(task_status_service.incomplete_tasks).to match_array(%i[missing_pages missing_privacy_policy_url missing_contact_details missing_what_happens_next share_preview_not_completed]) @@ -308,7 +309,7 @@ end describe "#task_statuses" do - let(:form) { create(:form, :live) } + let(:form) { create(:form, :live, :with_group, group:) } it "returns a hash with each of the task statuses" do expected_hash = { @@ -319,6 +320,7 @@ payment_link_status: :optional, privacy_policy_status: :completed, support_contact_details_status: :completed, + welsh_language_status: :optional, make_live_status: :completed, receive_csv_status: :optional, share_preview_status: :completed,